Introduction
Async context managers bring the safety of with statements to asynchronous resource management. They ensure that connections, transactions, file handles, and other async resources are properly opened and closed, even when exceptions occur. This lesson covers the async context manager protocol, the @asynccontextmanager decorator, combining multiple managers, and AsyncExitStack for dynamic scenarios.
Key Concepts
- Async context manager: An object implementing
__aenter__()and__aexit__()as async methods, used withasync with. - @asynccontextmanager: A decorator from
contextlibthat creates an async context manager from a generator function with a singleyield. - AsyncExitStack: A tool for managing a dynamic number of async context managers, useful when the number of resources is not known at compile time.
Real World Context
A web request handler needs a database connection, a Redis cache connection, and an HTTP session for making external API calls. Each resource requires async setup and teardown. Using async with for all three ensures they are properly closed in reverse order, even if the handler raises an exception. For a file processing tool that opens a variable number of files, AsyncExitStack manages them all.
Deep Dive
The Protocol
pythonclass AsyncContextManager: async def __aenter__(self): # Async setup return self async def __aexit__(self, exc_type, exc_val, exc_tb): # Async cleanup pass
Database Connection Example
pythonclass AsyncDatabase: async def __aenter__(self): self.connection = await asyncpg.connect(DSN) return self.connection async def __aexit__(self, exc_type, exc_val, exc_tb): await self.connection.close() async def main(): async with AsyncDatabase() as conn: result = await conn.fetch("SELECT * FROM users")
Using @asynccontextmanager
pythonfrom contextlib import asynccontextmanager @asynccontextmanager async def transaction(connection): tx = await connection.begin() try: yield tx await tx.commit() except Exception: await tx.rollback() raise async def main(): async with AsyncDatabase() as conn: async with transaction(conn) as tx: await tx.execute("INSERT ...")
Combining Multiple Context Managers
pythonasync def main(): async with ( AsyncDatabase() as db, AsyncCache() as cache, AsyncLogger() as logger ): # All resources available pass
ExitStack for Dynamic Context Managers
pythonfrom contextlib import AsyncExitStack async def process_files(filenames): async with AsyncExitStack() as stack: files = [ await stack.enter_async_context(aiofiles.open(f)) for f in filenames ] # All files automatically closed on exit
Common Pitfalls
- Raising exceptions in
__aexit__that mask the original error — If__aexit__raises an exception during cleanup, it replaces the original exception from theasync withbody. Usetry/exceptinside__aexit__to log cleanup errors without masking the original. - Forgetting to yield in @asynccontextmanager — The decorated function must contain exactly one
yield. Omitting it causes a confusingRuntimeError. - Not handling exceptions in the @asynccontextmanager — If you do not wrap
yieldintry/except, exceptions from the body will not trigger cleanup code like rollbacks.
Best Practices
- Use @asynccontextmanager for simple cases — The decorator is more concise than implementing a full class with
__aenter__and__aexit__. Reserve the class approach for complex stateful managers. - Use AsyncExitStack for dynamic resource management — When the number of resources varies at runtime (e.g., opening N files),
AsyncExitStackis cleaner than nestedasync withblocks.
Summary
- Async context managers ensure proper async setup and teardown of resources via
async with. - The protocol requires
__aenter__()and__aexit__()async methods. @asynccontextmanagercreates lightweight async context managers from generator functions.- Multiple async context managers can be combined in a single
async withstatement (Python 3.10+). AsyncExitStackmanages a dynamic number of async resources cleanly.
Code Examples
from contextlib import asynccontextmanager
import asyncio
@asynccontextmanager
async def timeout_context(seconds):
"""Timeout context manager."""
try:
async with asyncio.timeout(seconds):
yield
except asyncio.TimeoutError:
print(f"Operation timed out after {seconds}s")
raise
async def main():
try:
async with timeout_context(5):
await slow_operation()
except asyncio.TimeoutError:
print("Handling timeout...")