Async context managers
Overview
Async context managers allow you to manage resources that require asynchronous setup or teardown, such as network connections or database transactions. They use `async with` instead of `with` and must be run inside an `async def` function. The event loop awaits the setup before the block starts and awaits the teardown after it finishes. Class-based async context managers require two dunder methods: - `__aenter__(self)`: Awaited before the block executes to perform setup. RETURNS the object bound to the `as` variable (often `self`), or `None`. Raises an exception (like `ConnectionError`) if setup fails. - `__aexit__(self, exc_type, exc_val, tb)`: Awaited after the block executes to perform cleanup. RETURNS a boolean indicating if an exception should be suppressed (`True`), or propagated (`False` or `None`). Raises an exception if cleanup fails. @asynccontextmanager async def get_connection(): conn = await connect() yield conn await conn.close() # TRAP: Skipped if block raises! **The Generator Teardown Trap**: When using the `@asynccontextmanager` decorator, exceptions raised inside the `async with` block are thrown back into the generator at the `yield` statement. If you do not wrap the `yield` in a `try...finally` block, your teardown code will never execute.
Regular context managers block the event loop if their setup or teardown performs I/O. Async context managers let you `await` network calls or database operations safely while ensuring cleanup.
Where used: Database connection pools (`asyncpg`), HTTP sessions (`httpx` or `aiohttp`)
Why learn this
- Prevent blocking the `asyncio` event loop during resource setup and teardown.
- Correctly use modern `async` HTTP clients and database drivers.
Code walkthrough
import asyncio
class DB:
async def __aenter__(self):
print('Connecting...')
await asyncio.sleep(0)
return 'Connection'
async def __aexit__(self, *args):
print('Closing...')
async def run():
async with DB() as conn:
print(conn)
asyncio.run(run())
Focus: async with DB() as conn:
Aha moment
import asyncio
class SyncInAsync:
def __enter__(self):
print('Sync Enter')
def __exit__(self, *args):
print('Sync Exit')
async def run():
with SyncInAsync():
print('Inside')
asyncio.run(run())
Prediction: Is this code valid, or will it raise an error because `with` is used inside an `async` function?
Common guess: It will raise an error.
It is valid! You can use regular `with` context managers inside `async` functions. You only need `async with` if the context manager itself is asynchronous (i.e., performs I/O).
Common mistakes
- Using `with` instead of `async with`: If you use a regular `with` statement on an async context manager, Python will raise an `AttributeError` because it looks for `__enter__` instead of `__aenter__`.
- Forgetting async def on dunder methods: If you define `__aenter__` or `__aexit__` as regular functions instead of `async def`, Python cannot `await` them, resulting in a `TypeError`.
- Omitting try/finally in @asynccontextmanager: If you do not wrap the `yield` in a `try...finally` block, exceptions raised inside the `async with` block will crash the generator. Any teardown code placed after the `yield` will be skipped, leading to resource leaks.
Glossary
- event loop
- The core mechanism that manages and runs multiple asynchronous tasks efficiently, e.g. `asyncio.run(main())`.
- I/O
- Input/Output operations, like reading from a database or a network, which often take time to complete, e.g. `await db.fetch('SELECT ...')`.
Recall questions
- Which keyword must precede the `with` statement to use an async context manager?
- What two dunder methods are required to implement a class-based async context manager?
- Which decorator from `contextlib` is used to create an async context manager from a generator?
- What happens to exceptions raised in the `async with` block when using `@asynccontextmanager`?
Understanding checks
What is printed to the console?
A B C
`async with` awaits `__aenter__` printing `A`, runs the block printing `B`, and then awaits `__aexit__` printing `C`.
What is printed to the console?
Acquire Work Release
Using `@asynccontextmanager`, the code before `yield` runs first (`Acquire`), then the block runs (`Work`), and finally the code after `yield` runs (`Release`).
What is printed to the console?
Acquire Work Caught
The `ValueError` raised inside the block is thrown into the `lock` generator at the `yield` statement. Because `yield` is not wrapped in a `try...finally` block, the exception halts the generator, skipping `Release`.
Practice tasks
Migrate to Async
Given this synchronous context manager using `@contextmanager`, convert it to an `async` context manager using `@asynccontextmanager`.
Challenge
An Async Error Suppressor
Write an `async` context manager class `AsyncSuppressor` that catches and suppresses `ValueError`. Both `__aenter__` and `__aexit__` must be implemented correctly.
Continue learning
Previous: __enter__ and __exit__
Previous: contextlib.contextmanager decorator