async with, async for and async generators

asyncio · Advanced · 12 min

Connection pools, sessions and streaming APIs all expose async with / async for interfaces.

Two protocols let objects await during setup, teardown and iteration.

Async context managers implement __aenter__ and __aexit__ (both coroutines) and are used with async with. The easiest way to write one is contextlib.asynccontextmanager: an async generator that does setup, yields the resource once, and cleans up in finally.

Async iterators implement __aiter__ (a regular method returning the iterator) and async def __anext__, which raises StopAsyncIteration at the end. Again, an async generator (an async def containing yield) is far simpler: you can await between yields, which is ideal for paginated APIs or reading a stream line by line.

async def pages(client):
    url = START
    while url:
        page = await client.get(url)
        yield page.items
        url = page.next

Async comprehensions work too: [x async for x in gen()], and await can appear inside comprehensions within async functions. Built-ins aiter() and anext() mirror iter() and next().

One subtlety: an async generator abandoned mid-iteration is finalized later by the event loop, so its finally may run late. Use contextlib.aclosing(gen) to close it deterministically when you break early.

Example

import asyncio
from contextlib import asynccontextmanager, aclosing

@asynccontextmanager
async def connection(name):
    print(f"open {name}")
    try:
        yield name.upper()
    finally:
        await asyncio.sleep(0)          # async cleanup is allowed
        print(f"close {name}")

async def ticker(n):
    for i in range(n):
        await asyncio.sleep(0.01)
        yield i

async def main():
    async with connection("db") as conn:
        print("using", conn)
        async with aclosing(ticker(10)) as t:
            print([i async for i in t if i % 2][:3])   # [1, 3, 5]

asyncio.run(main())
# open db / using DB / [1, 3, 5] / close db

Remember

  • async with calls awaitable __aenter__/__aexit__
  • An async def with yield is an async generator, used with async for
  • asynccontextmanager and aclosing make these easy and deterministic

Watch out for

  • Using a plain for loop or with statement on async objects; they need async for / async with.

Check yourself

  1. Which method must an async iterator's __anext__ raise when it is exhausted?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager