tezvyn:

Python Async Context Managers

AI-drafted, machine-checkedSource: docs.python.orgadvanced
Python Async Context Managers

Async context managers let you await during setup and teardown. Use async with for database connections or streams where acquiring and releasing both need I/O. The footgun is applying @contextmanager to async cleanup, which cannot await and will crash.

WHY IT EXISTS: Synchronous context managers handle cleanup with the with statement, but modern Python services spend most of their time waiting on I/O. If acquiring or releasing a resource needs an await, such as connecting to a PostgreSQL database or closing an HTTP session, the standard enter and exit methods cannot suspend execution. Blocking the event loop to perform cleanup defeats the purpose of async code. Asynchronous context managers were added so that setup and teardown could both yield control back to the event loop, letting other coroutines run while the resource waits to open or close.

THE MENTAL MODEL: Think of a regular context manager as a light switch that you flip on and off instantly. An async context manager is like a smart home switch that must send a network command to turn the lamp on or off, and the switch returns only after the device acknowledges the signal. You still get the same try-finally guarantee, but both arms of the transaction can pause.

HOW IT WORKS: You create one by either implementing __aenter__ and __aexit__ on a class, or by decorating an async generator with @asynccontextmanager from the contextlib module. The decorated function must yield exactly one value, which becomes the bound variable in the async with statement. Code before the yield runs as setup and can await. Code after the yield, usually in a finally block, runs as teardown and can also await. If the body of the async with raises an exception, it is injected back into the generator at the yield point so you can catch or re-raise it.

WHEN TO USE IT: Use it whenever a resource lifecycle crosses an async boundary. Common cases include acquiring and releasing database connections, opening and flushing websocket streams, entering and exiting distributed locks over the network, or metering async API calls where rate-limit cleanup requires an HTTP request.

WHEN NOT TO USE IT: Do not use it for purely synchronous resources because the overhead of an async generator and event loop coordination is wasted if no suspension is needed. Also avoid it if you are tempted to put heavy CPU work in __aenter__ or __aexit__ without an await, because that still blocks the event loop. Finally, do not mix the sync @contextmanager decorator with async cleanup; that decorator is for synchronous generators only and cannot await.

ONE CANONICAL EXAMPLE: The docs show a database connection factory. You define async def get_connection, assign conn via await acquire_db_connection, then in a try block yield conn, and in a finally block await release_db_connection on conn, decorating the function with @asynccontextmanager. A caller then writes async with get_connection as conn and runs conn.query. Even if the query raises, the finally block runs and the connection is released asynchronously.

Source: docs.python.org

Read the original → docs.python.org

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.