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())
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.