Async context managers

RoadmapsPython

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

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

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

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

Return to Python Roadmap