pyguides

__aenter__ / __aexit__

What __aenter__ and __aexit__ Do

__aenter__ and __aexit__ are the two methods that make a class usable with async with. Together they form the async context manager protocol, introduced by PEP 492 in Python 3.5. When you write async with SomeResource() as resource:, Python calls __aenter__ on entry and __aexit__ on exit, regardless of how the block ends.

These are the asynchronous counterparts to __enter__ and __exit__. You use them when your setup or teardown code needs to await something — an async database connection, a network session, a lock. The sync protocol (with) cannot handle that; trying to use a regular def for these methods inside async with raises a TypeError at runtime.

__aenter__(self)

async def __aenter__(self):
    ...

Python calls __aenter__ when execution enters the async with block. The method must be defined with async def — a regular def will cause a TypeError when Python attempts to await it.

The return value is what gets bound to the name after as. If you write async with MyResource() as res:, the variable res receives whatever __aenter__ returns. Most of the time this is self, so you get the whole object. But you can return something else — for example, aiohttp’s ClientSession.__aenter__ returns the session itself, while a database wrapper might return only the connection object.

import asyncio

class AsyncDatabase:
    async def __aenter__(self):
        # Simulate opening a connection
        self.connection = await asyncio.sleep(0, result="<db-connection>")
        print("Connection opened")
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("Connection closed")
        self.connection = None
        return False

async def main():
    async with AsyncDatabase() as db:
        print(f"Using: {db.connection}")
    # __aexit__ called here, connection is None

__aexit__(self, exc_type, exc_val, exc_tb)

async def __aexit__(self, exc_type, exc_val, exc_tb):
    ...

Python calls __aexit__ when execution leaves the async with block — whether it completes normally, hits a return, or raises an exception. All three parameters are None if the block exited without an error.

ParameterDescription
exc_typeThe exception type (e.g. ValueError), or None
exc_valThe exception instance, or None
exc_tbThe traceback object, or None

The return value controls exception handling. If __aexit__ returns True (or any truthy value), Python suppresses the exception as if it never happened — execution continues after the async with block normally. This is useful when you want to catch and handle an error internally.

class SuppressingDB:
    async def __aenter__(self):
        self.connection = "<open-conn>"
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await asyncio.sleep(0)  # simulate async close
        self.connection = None
        if exc_type is ValueError:
            print(f"Swallowing ValueError: {exc_val}")
            return True  # suppress the exception
        return False  # re-raise any other exception

async def main():
    async with SuppressingDB() as db:
        raise ValueError("not a real error")
    print("Continued after suppressed exception")

Most context managers return False (or None) and let exceptions propagate. Only return True when you genuinely want to swallow an exception.

Sync vs Async Context Manager Protocol

The two protocols are entirely separate. Python decides which one to use based on whether you write with or async with.

Feature__enter__ / __exit____aenter__ / __aexit__
Statementwithasync with
Method definitionregular defasync def
Can await inside setupNoYes
First Python version2.5 (PEP 343)3.5 (PEP 492)

You cannot mix them. A class that only defines __enter__ and __exit__ cannot be used with async with. And a class that only defines __aenter__ and __aexit__ cannot be used with the plain with statement.

Real-World Pattern: Async Database Connection

This is the most common use case for async context managers. You want to connect when you enter the block and disconnect when you leave, even if something fails mid-way.

import asyncio

class AsyncDBConnection:
    async def __aenter__(self):
        # In real code this would be await aiopg.connect() or similar
        await asyncio.sleep(0.1)  # simulate connection setup
        self._conn = "<live-db-connection>"
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # In real code this would be await self._conn.close()
        await asyncio.sleep(0.05)  # simulate graceful close
        self._conn = None
        return False  # do not suppress exceptions

async def run_query(sql):
    async with AsyncDBConnection() as conn:
        print(f"Executing: {sql} via {conn._conn}")
        await asyncio.sleep(0.1)  # simulate query

asyncio.run(run_query("SELECT * FROM users"))
# Connection opened
# Executing: SELECT * FROM users via <live-db-connection>
# Connection closed

Real-World Pattern: aiohttp.ClientSession

The aiohttp.ClientSession class is itself an async context manager. Under the hood it implements __aenter__ and __aexit__ to handle session creation and teardown. You never need to implement these methods from scratch when working with it — you just use the session with async with.

import asyncio
import aiohttp

async def fetch_json(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

async def main():
    # This opens a session, makes a GET request, and closes the session.
    # __aenter__ of ClientSession is called on the outer async with.
    # __aexit__ of ClientSession is called when the block ends.
    pass

Understanding that aiohttp.ClientSession implements the async context manager protocol makes it clear why you must use async with with it — the sync with statement simply has no effect on an object that only implements the async variant.

Building Async Context Managers with @asynccontextmanager

Writing a full class just to get async setup and teardown is often overkill. The contextlib.asynccontextmanager decorator lets you create an async context manager from an async generator function. Everything before yield runs as __aenter__; everything after yield runs as __aexit__.

import asyncio
from contextlib import asynccontextmanager

@asynccontextmanager
async def async_db_connection(dsn):
    # __aenter__ runs this
    conn = await asyncio.sleep(0, result=f"<conn:{dsn}>")
    print(f"Connected to {dsn}")
    try:
        yield conn
    finally:
        # __aexit__ runs this
        await asyncio.sleep(0, result=None)  # simulate close
        print(f"Disconnected from {dsn}")

async def main():
    async with async_db_connection("postgresql://localhost") as conn:
        print(f"Using {conn}")
    # Disconnected here

asyncio.run(main())

The try/finally around yield is not required by the decorator — it runs your finally block regardless of how the async with block exits. This gives you the same guarantee as a class-based context manager.

Cleaning Up Async Generators with aclosing()

Async generators (async def functions that yield) do not automatically close when you stop iterating them. If you break out of an async for loop early, the generator’s aclose() method is never called and the generator sits in a suspended state. The contextlib.aclosing() context manager fixes this.

import asyncio
from contextlib import aclosing

async def async_page_crawler(urls):
    for url in urls:
        yield f"result-from-{url}"
        await asyncio.sleep(0.1)

async def main():
    pages = async_page_crawler(["a.com", "b.com", "c.com"])
    async with aclosing(pages) as agen:
        async for page in agen:
            print(page)
            if "b.com" in page:
                break  # aclosing guarantees .aclose() even on early break
    # Generator properly closed here

asyncio.run(main())

Without aclosing, breaking early would leave the generator coroutine dangling. aclosing wraps the generator in an async context manager that calls .aclose() in its __aexit__.

Relationship to Async Iteration

The async context manager protocol and the async iteration protocol are separate things introduced by the same PEP, but they are not the same.

ProtocolMethodsStatement
Async Context Manager__aenter__, __aexit__async with
Async Iterator__aiter__, __anext__async for

An async iterator implements __aiter__ and __anext__ and is consumed by async for. An async context manager implements __aenter__ and __aexit__ and is consumed by async with. You will often use them together — aclosing is the clearest example, since it wraps an async iterator in an async context manager.

Note that PEP 492 changed __aiter__ in Python 3.5.2: __aiter__ must return an asynchronous iterator directly, not an awaitable. The old protocol (returning an awaitable) was deprecated in Python 3.6 and removed in 3.7.

See Also

  • __await__ — makes an object awaitable in await expressions
  • __aiter__ / __anext__ — implements async iteration with async for
  • Context Managers — a complete guide to with, async with, contextlib, and resource cleanup patterns