Python Async Generators Explained

How async def combined with yield actually works, when to reach for one over a regular generator or async function, and what’s changing in Python 3.16

An async generator is a function that combines async def with yield — it produces values lazily, one at a time, like a regular generator, but can await other coroutines between values, like an async function. You consume it with async for instead of a plain for loop.

Where it sits between two things you already know

A plain generator produces values lazily with yield, but has no way to await anything inside it — everything it does has to be synchronous. A plain async function can await, but only ever returns once, at the end. An async generator does both at once: it can await between each yield, producing a lazy stream of values where each one might depend on some asynchronous work.

import asyncio

async def fetch_pages(base_url, total_pages):
    for page in range(1, total_pages + 1):
        data = await fetch_page(base_url, page)  # an async network call
        yield data  # hand back this page, then pause here

async def main():
    async for page_data in fetch_pages("https://api.example.com/items", 5):
        print(page_data)

asyncio.run(main())

Nothing runs when fetch_pages(...) is called — like a regular generator, it returns an async generator object immediately. Code only executes as async for consumes it, one await-then-yield cycle at a time.

Why not just use a list of awaited results?

You could await every page upfront and return a plain list, but that means holding everything in memory and waiting for the entire sequence to finish before processing the first item. An async generator lets you start working with page 1 the moment it arrives, without waiting on pages 2 through 5 — the same memory and latency benefit regular generators give you over building a full list, applied to async work.

Consuming one correctly

async for is the normal way to iterate an async generator. You can also drive it manually with asend() (push a value in, like send() on a regular generator), athrow() (raise an exception inside it), and aclose() (stop it early and let it clean up) — but most code never needs those directly; they matter mainly to library authors building abstractions on top of async generators.

What’s changing: PEP 828 in Python 3.16

Today, if one async generator wants to delegate to another, there’s no async equivalent of a plain generator’s yield from — you have to write a manual async for inner_value in other_gen(): yield inner_value loop, which doesn’t forward asend(), athrow(), or aclose() calls through to the inner generator, and loses any return value the inner generator might carry. PEP 828, accepted August 2026 and targeting Python 3.16, adds real yield from support (and an async yield from form) to fix exactly this — full delegation, symmetric with how synchronous generators already work.

The practical guideline

  • Need lazy values with no async work involved → a regular generator is simpler and sufficient.
  • Need one async result, not a stream → a plain async function is the right shape.
  • Need a lazy stream where producing each item involves awaiting something → an async generator, consumed with async for — API pagination, chunked async reads, and streaming database drivers are the common real-world cases.

Frequently Asked Questions

What is an async generator in Python?

A function defined with async def that also uses yield. Like a regular generator, it produces values lazily one at a time; like an async function, it can await other coroutines between values. You iterate it with async for instead of a plain for loop.

What’s the difference between a generator, an async function, and an async generator?

A generator (yield, no async) produces values lazily but can’t await anything. An async function (async def, no yield) can await, but returns one final value. An async generator (async def with yield) does both — it awaits between values and produces them lazily, one at a time.

When do you actually need an async generator?

Streaming data that requires an async operation to produce each item — paginating through an API where each page is a separate await’d request, reading a large response body in async chunks, or streaming rows from an async database driver — without loading everything into memory first.

Is yield from coming to async generators?

Yes — PEP 828, accepted August 2026, adds yield from support to async generators in Python 3.16. Today, delegating to another async generator requires a manual async for loop that can’t forward asend(), athrow(), or aclose() calls through to the inner generator.