Python Async Iterators — Core Concepts
The Async Iteration Protocol
Just as regular iterators implement __iter__ and __next__, async iterators implement two special methods:
__aiter__— returns the async iterator object (usuallyself)__anext__— returns an awaitable that resolves to the next value, or raisesStopAsyncIterationwhen exhausted
class Countdown:
def __init__(self, start):
self.current = start
def __aiter__(self):
return self
async def __anext__(self):
if self.current <= 0:
raise StopAsyncIteration
self.current -= 1
await asyncio.sleep(1) # Simulate waiting
return self.current + 1
Usage with async for:
async for number in Countdown(5):
print(number) # Prints 5, 4, 3, 2, 1 — one per second
async for Under the Hood
When Python encounters async for item in source, it does:
- Calls
source.__aiter__()to get the iterator - Repeatedly calls
await iterator.__anext__()to get values - Assigns each value to
itemand runs the loop body - Stops when
StopAsyncIterationis raised
The crucial difference from regular for: step 2 uses await, meaning the event loop can run other tasks while waiting for the next value.
Async Generators: The Easy Way
Writing a class with __aiter__ and __anext__ is verbose. Python 3.6+ provides async generators — functions that use both async def and yield:
async def fetch_pages(urls):
async with aiohttp.ClientSession() as session:
for url in urls:
async with session.get(url) as response:
yield await response.text()
This creates an async iterator automatically. No class needed.
async for page_html in fetch_pages(url_list):
process(page_html)
When to Use Async Iterators
Streaming data: When data arrives piece by piece — WebSocket messages, server-sent events, paginated API results.
Large datasets: Instead of loading everything into memory, process rows one at a time:
async def query_large_table(conn):
async for row in conn.cursor("SELECT * FROM huge_table"):
yield transform(row)
Event-driven patterns: Listening for events indefinitely:
async def event_listener(queue):
while True:
event = await queue.get()
if event is None:
break
yield event
Async Iterators vs Regular Iterators
| Feature | Regular Iterator | Async Iterator |
|---|---|---|
| Protocol | __iter__, __next__ | __aiter__, __anext__ |
| Loop syntax | for x in iter | async for x in aiter |
| Can await | No | Yes |
| Stop signal | StopIteration | StopAsyncIteration |
| Works in sync code | Yes | No — needs async def context |
Common Misconception
“Async iterators process items in parallel.” They don’t. Each __anext__ call is awaited sequentially — you get one item, process it, then get the next. The benefit isn’t parallelism; it’s non-blocking waiting. While one async iterator waits for its next item, the event loop can service other coroutines.
If you need to consume items from multiple async iterators simultaneously, you’d use asyncio.TaskGroup or libraries like aiostream.
One thing to remember: Async iterators implement __aiter__ and __anext__ to deliver items with non-blocking waits between them. Use async generators (async def + yield) for the simplest way to create them.
See Also
- Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
- Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
- Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
- Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
- Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.