Skip to content

Async Context Managers and Async Generators

diagram async with and async for call different methods mermaid
These are not the ordinary protocols with an await bolted on -- they are separate dunder methods, and an object supporting one does not support the other. That is why a synchronous context manager cannot be used with async with, and why an async generator cannot be driven by a plain for loop.

You use async with when acquiring/releasing a resource needs await.

Common examples:

  • aiohttp.ClientSession()
  • async database connections
  • async locks (asyncio.Lock)
aiohttp_context_manager.py
import asyncio
import aiohttp
 
 
async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get("https://api.github.com") as resp:
            data = await resp.json()
            print(resp.status)
            print("keys:", list(data.keys())[:5])
 
 
asyncio.run(main())

Use @asynccontextmanager for a clean pattern.

custom_async_context_manager.py
import asyncio
from contextlib import asynccontextmanager
 
 
@asynccontextmanager
async def managed_resource(name: str):
    print("acquire", name)
    await asyncio.sleep(0.1)
    try:
        yield {"name": name}
    finally:
        print("release", name)
        await asyncio.sleep(0.1)
 
 
async def main():
    async with managed_resource("db-conn") as res:
        print(res)
 
 
asyncio.run(main())

An async generator is a generator that can await between yields.

It’s perfect for streaming data without loading everything into memory.

async_generator.py
import asyncio
 
 
async def ticker(n: int, delay: float = 0.2):
    for i in range(n):
        await asyncio.sleep(delay)
        yield i
 
 
async def main():
    async for value in ticker(5):
        print(value)
 
 
asyncio.run(main())
  • Use async with only with objects that implement __aenter__ / __aexit__.
  • Use async for only on async iterables (implement __aiter__).
  • Don’t forget to close async resources (sessions, connections). async with helps you avoid leaks.
sketch async with and async for use different methods entirely p5.js
These are not the ordinary protocols with an await bolted on. They are separate dunder methods, so an object supporting one does not support the other -- which is why a synchronous context manager fails under async with, and an async generator cannot be driven by a plain for loop. The order shown was observed from a real run.
pch.quizTag pch.quizDefaultTitle
  1. Which methods does `async with` call?

    pch.quizShowAnswer

    B — `__aenter__` and `__aexit__` — They are separate dunder methods, not awaited versions of the sync ones. That is why a synchronous context manager cannot be used with `async with` at all.

  2. You use `async with` on a class that defines `__enter__`/`__exit__`. What happens?

    pch.quizShowAnswer

    B — TypeError naming the missing `__aexit__`, and 3.14 suggests using `with` instead — Verified. The message reads: does not support the asynchronous context manager protocol (missed __aexit__ method) but it supports the context manager protocol. Did you mean to use 'with'?

  3. Why is `__aexit__` worth having over an ordinary `finally`?

    pch.quizShowAnswer

    B — It is awaited, so cleanup can do I/O — closing a connection properly — Both `__aenter__` and `__aexit__` are awaited, which is the usual reason to write one: acquiring and releasing something over the network.

  4. A plain `for` loop over an async generator does what?

    pch.quizShowAnswer

    B — `TypeError: 'async_generator' object is not iterable` — `__anext__` returns an awaitable rather than a value, so the ordinary iteration protocol does not apply. Use `async for`.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading