Introduction
While async generators are the quick way to produce async sequences, async iterators give you full control over the iteration protocol. By implementing __aiter__ and __anext__, you can build custom async data sources like database cursors, WebSocket message streams, and event listeners. This lesson covers the async iterator protocol, practical implementations, the aiter()/anext() built-ins, and how to merge multiple async streams.
Key Concepts
- Async iterator protocol: An object that implements
__aiter__()(returns the iterator) and__anext__()(an async method that returns the next value or raisesStopAsyncIteration). - StopAsyncIteration: The exception that signals the end of an async iterator, analogous to
StopIterationfor sync iterators. - aiter() / anext(): Built-in functions (Python 3.10+) for working with async iterators, analogous to
iter()andnext().
Real World Context
A real-time stock ticker application receives price updates from multiple exchanges via WebSocket. Each exchange is wrapped in an async iterator. A merge_streams() function combines them into a single async stream, yielding the latest price from whichever exchange responds first. The consumer processes prices with async for, unaware of how many exchanges are feeding the stream.
Deep Dive
The Protocol
pythonclass AsyncIterator: def __aiter__(self): return self async def __anext__(self): # Return next value or raise StopAsyncIteration pass
Example: Database Cursor
pythonclass AsyncCursor: def __init__(self, query): self.query = query self.results = None self.index = 0 def __aiter__(self): return self async def __anext__(self): if self.results is None: self.results = await execute_query(self.query) if self.index >= len(self.results): raise StopAsyncIteration item = self.results[self.index] self.index += 1 return item async def main(): async for row in AsyncCursor("SELECT * FROM users"): print(row)
aiter() and anext() Built-ins
python# Python 3.10+ async def main(): ait = aiter(async_generator()) # Get first item first = await anext(ait) # With default item = await anext(ait, "default")
Combining Async Iterators
pythonasync def merge_streams(*streams): """Merge multiple async iterators.""" pending = set() iterators = {aiter(s): s for s in streams} for it in iterators: pending.add(asyncio.create_task(anext(it, StopAsyncIteration))) while pending: done, pending = await asyncio.wait( pending, return_when=asyncio.FIRST_COMPLETED ) for task in done: result = task.result() if result is not StopAsyncIteration: yield result
Common Pitfalls
- Making
__aiter__async —__aiter__must be a regular method (notasync def) that returns the iterator object. Making it async causesasync forto fail with aTypeError. - Forgetting
StopAsyncIteration— If__anext__never raisesStopAsyncIteration, theasync forloop runs forever. Always ensure there is a termination condition. - Reusing an exhausted iterator — Once an async iterator raises
StopAsyncIteration, it is spent. Create a new instance to iterate again. Consider implementing__aiter__to return a fresh iterator if reuse is needed.
Best Practices
- Use async generators for simple cases — If your async iterator is just yielding values from an async source, an async generator is simpler and less error-prone than a full class implementation.
- Use
anext()with a default for safe single-item consumption —await anext(ait, None)returnsNoneif the iterator is exhausted instead of raising an exception.
Summary
- Async iterators implement
__aiter__()and__anext__()to supportasync forloops. StopAsyncIterationsignals the end of the async sequence.aiter()andanext()(Python 3.10+) provide convenient built-in access to async iterators.- Multiple async streams can be merged by scheduling
anext()tasks and processing results as they complete. - Prefer async generators for simple use cases; use the full protocol for complex stateful iterators.
Code Examples
class AsyncFileReader:
"""Read file lines asynchronously."""
def __init__(self, filename, chunk_size=8192):
self.filename = filename
self.chunk_size = chunk_size
self.file = None
self.buffer = ""
def __aiter__(self):
return self
async def __anext__(self):
while "\n" not in self.buffer:
if self.file is None:
self.file = await aiofiles.open(self.filename)
chunk = await self.file.read(self.chunk_size)
if not chunk:
if self.buffer:
line = self.buffer
self.buffer = ""
return line
await self.file.close()
raise StopAsyncIteration
self.buffer += chunk
line, self.buffer = self.buffer.split("\n", 1)
return line