Skip to main content
PEP 525 · Speedrun · Python 3.6 (2016)

PEP 525 in 90 seconds — async generators and streaming-as-default

Every async streaming API you've ever used is built on this.

PEP 380 (`yield from`) made nested sync generators clean. PEP 525 (Python 3.6) closed the asyncio gap — `async def` functions can now contain `yield`. The shape: ```python async def stream_chunks(response): async for chunk in response.aiter_bytes(): yield chunk ``` Pre-3.6, streaming an HTTP body, SSE feed, or paginated DB result required manual `Queue` plumbing or callback hell. With async generators, the consumer becomes a one-liner: `async for x in source: ...`.

Two protocol additions ship together: `__aiter__` (the async equivalent of `__iter__`) and `__anext__` (async `__next__`). Every modern Python async library is built on them — `httpx.aiter_bytes()`, `aiohttp` response streams, `asyncpg` cursors, the Anthropic SDK's `messages.stream(...)`, every LLM provider's streaming endpoint. If you've used `async for` once, you've used PEP 525.

Before — Python ≤ 3.5, manual queue + sentinel
import asyncio

async def producer(queue, n):
    for i in range(n):
        await queue.put(i ** 2)
    await queue.put(None)   # sentinel meaning "done"

async def consumer(queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(item)
After — Python 3.6+, async def with yield
import asyncio

async def squares(n):
    for i in range(n):
        yield i ** 2          # async def + yield = async generator
        await asyncio.sleep(0) # cooperative yield to the loop

async def main():
    async for sq in squares(4):
        print(sq)

asyncio.run(main())
# Prints 0 1 4 9 one per line.

🎯 Predict the output

What does this print? `async for ... in async_gen` collects values just like a sync `for ... in gen` would.

import asyncio

async def numbers(n):
    for i in range(n):
        yield i ** 2

async def main():
    result = [x async for x in numbers(4)]
    print(result)

asyncio.run(main())
async/await deep dive → Foundations track

Or speedrun another PEP

PEP 572Assignment as an expression — 3 lines become 1.
PEP 572 in 90 seconds — the walrus that ate your for-loop
PEP 484Python stays dynamic — but you can opt into typing.
PEP 484 in 90 seconds — type hints, without TypeScript-envy
PEP 622Switch statements are a 5% feature. Pattern matching is the 95%.
PEP 622 in 90 seconds — match / case isn't just a switch