__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.
| Parameter | Description |
|---|---|
exc_type | The exception type (e.g. ValueError), or None |
exc_val | The exception instance, or None |
exc_tb | The 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__ |
|---|---|---|
| Statement | with | async with |
| Method definition | regular def | async def |
| Can await inside setup | No | Yes |
| First Python version | 2.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.
| Protocol | Methods | Statement |
|---|---|---|
| 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 inawaitexpressions__aiter__/__anext__— implements async iteration withasync for- Context Managers — a complete guide to
with,async with,contextlib, and resource cleanup patterns