C.W.K.
Stream
Lesson 05 of 07 · published

Async Iteration — async for, __aiter__, __anext__

~18 min · async, async-for, async-iter, asyncio

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The async cousin of the iteration protocol

Regular iteration: __iter__ + __next__. Async iteration: __aiter__ + __anext__. The difference is that __anext__ is a coroutine — it can await something before producing the next value. The consumer side is async for x in stream:. This is the protocol that powers streaming responses, async DB cursors, websocket message loops, anything where producing each value involves I/O.

Async generators — the easy way

Just like regular generators replace most class-based iterators, async generators replace most class-based async iterators. Define an async def function that uses yield. The function becomes an async generator. async for consumes it. await can appear before each yield to do I/O. cwkPippa's adapters use this pattern everywhere — chunks come in over the network, get processed, and stream out via yield.

Async comprehensions

[x async for x in stream] is an async list comprehension. (x async for x in stream) is an async generator expression. Both can only appear inside async def. They're convenient when you're already in an async context.

Principle: Sync iteration and async iteration are different protocols. You can't use async for on a regular iterable, and you can't use for on an async iterable. The error messages are clear ("not async iterable" / "not iterable"); knowing the protocols saves time decoding them.

Where this comes up in real code

Web frameworks streaming response chunks. Database cursors yielding rows as they arrive. AI SDKs (like the Claude Agent SDK) yielding tokens, tool calls, and lifecycle events. WebSocket message loops. Anywhere you'd say "process each item as it shows up." The Concurrency track covers asyncio in depth; this lesson is just the iteration shape.

Code

Async generator — the simple shape·python
import asyncio

async def ticker(n):
    for i in range(n):
        await asyncio.sleep(0.1)        # do some I/O
        yield i

async def main():
    async for x in ticker(3):
        print(x)
# 0
# 1
# 2  (each ~100ms apart)

asyncio.run(main())
async for — the consumer side·python
import asyncio

async def chunks():
    for piece in ["hello ", "world ", "from ", "pippa"]:
        await asyncio.sleep(0.05)
        yield piece

async def collect():
    full = ""
    async for chunk in chunks():
        full += chunk
    return full

result = asyncio.run(collect())
print(result)                   # 'hello world from pippa'
Async comprehension·python
import asyncio

async def naturals(limit):
    for n in range(1, limit + 1):
        await asyncio.sleep(0.01)
        yield n

async def main():
    # async list comprehension
    squares = [x*x async for x in naturals(5)]
    print(squares)              # [1, 4, 9, 16, 25]

    # async generator expression
    total = sum([x*x async for x in naturals(5)])
    print(total)                # 55

asyncio.run(main())
Class-based async iterator (when you need state)·python
import asyncio

class DelayedRange:
    def __init__(self, n, delay=0.05):
        self.n = n
        self.delay = delay
        self.i = 0

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self.i >= self.n:
            raise StopAsyncIteration
        await asyncio.sleep(self.delay)
        v = self.i
        self.i += 1
        return v

async def main():
    async for x in DelayedRange(4):
        print(x)

asyncio.run(main())
Wrong protocol — clear error messages·python
import asyncio

async def main():
    # async for over a regular iterable — error
    try:
        async for x in [1, 2, 3]:
            pass
    except TypeError as e:
        print(e)                # 'list' object is not async iterable

    # for over an async iterable — error
    async def stream():
        yield 1

    try:
        for x in stream():
            pass
    except TypeError as e:
        print(e)                # 'async_generator' object is not iterable

asyncio.run(main())

External links

Exercise

Write an async generator tick(every, count) that yields the integers 0, 1, ..., count-1 with every seconds of await asyncio.sleep between each. Then write an async function that uses async for to consume it, prints each value with the elapsed time. Then rewrite the consumer using an async list comprehension to collect the values into a list and print them all at the end. Run with asyncio.run.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.