Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back.
In our last lesson, we explored the why behind asynchronous iteration. We established that for I/O-bound data streams, a standard for loop would block the event loop, defeating the purpose of asyncio. The async for statement was introduced as the solution to this problem, allowing an application to remain responsive while iterating over data that arrives incrementally.
Today, we shift our focus from the "why" to the "how." This lesson dives into the mechanics that power the async for loop. We will define the asynchronous iteration protocol, which is the formal set of rules and methods Python uses to perform asynchronous iteration. By the end of this lesson, you will be able to detail the precise roles of the __aiter__ and __anext__ methods, which are the core components of this protocol.
1. The Protocol Analogy: From Synchronous to Asynchronous
To understand the asynchronous protocol, it's helpful to first recall its synchronous counterpart, which you've already mastered. A standard for loop:
for item in iterable:
...is, under the hood, roughly equivalent to this:
# 1. Get the iterator from the iterable
iterator = iter(iterable) # Calls iterable.__iter__()
while True:
try:
# 2. Get the next item from the iterator
item = next(iterator) # Calls iterator.__next__()
except StopIteration:
# 3. Stop when the iterator is exhausted
break
# ... do something with item ...
The process relies on three key elements:
- The
__iter__method, which returns an iterator object. - The
__next__method, which returns the next item. - The
StopIterationexception, which signals the end of the iteration.
The asynchronous iteration protocol mirrors this structure almost exactly, but with async/await woven in to handle the non-blocking waits.
2. Defining the Asynchronous Iteration Protocol
An async for loop:
async for item in async_iterable:
...is conceptually translated by Python into something like this:
# 1. Get the async iterator from the async iterable
async_iterator = async_iterable.__aiter__()
while True:
try:
# 2. AWAIT the next item from the async iterator
item = await async_iterator.__anext__()
except StopAsyncIteration:
# 3. Stop when the async iterator is exhausted
break
# ... do something with item ...
Notice the key differences:
__iter__becomes__aiter__.__next__becomes__anext__, and crucially, it is awaited.StopIterationbecomesStopAsyncIteration.
The following video provides a very clear and concise explanation of this protocol.
This segment from the mCoding video 'Async for loops in Python' directly explains the low-level asynchronous iterator protocol, breaking down the roles of __aiter__ and __anext__.
Please watch the segment 'The Asynchronous Iterator Protocol (aiter and anext)' from 12:55 to 14:16. Focus on how the async for loop is deconstructed into calls to these dunder methods.
3. A Closer Look at the Protocol's Components
Let's dissect the two methods and the exception that form the protocol.
__aiter__(self)
- Role: Its single responsibility is to return an asynchronous iterator object—an object that has an
__anext__method. - Signature: It is a regular, synchronous method (
def, notasync def). The process of getting the iterator is not expected to be an I/O-bound operation. - Convention: For an object that is itself an asynchronous iterator, its
__aiter__method should simplyreturn self. This convention makes all asynchronous iterators also asynchronous iterables, allowing them to be used directly in anasync forloop. This parallels the synchronous world where all iterators are also iterables.
__anext__(self)
- Role: This is the heart of the protocol. It is responsible for fetching the next item in the sequence. If the process involves waiting (e.g., for a network packet to arrive, a database query to return a row), it should use
await. - Signature: It must be a coroutine method, defined with
async def. This is what allows theasync forloop toawaitits result, yielding control to the event loop during any I/O waits. - Return Value: On success, it returns the next item.
- Termination: When there are no more items to return, it must raise the
StopAsyncIterationexception. Raising the standardStopIterationhere would result in aRuntimeError, as the two protocols are distinct.
4. A Minimal Implementation
Let's solidify this with a minimal, conceptual example. The following class implements an asynchronous iterator that counts from 0 to a specified limit, pausing for a short duration between each number to simulate a non-blocking I/O operation.
import asyncio
import time
class AsyncCounter:
"""
An asynchronous iterator that counts from 0 up to (but not including) 'limit',
pausing between each number.
"""
def __init__(self, limit):
self.limit = limit
self.counter = 0
def __aiter__(self):
"""Returns the iterator object itself."""
print("-> __aiter__ called")
return self
async def __anext__(self):
"""
Returns the next number in the sequence.
Pauses asynchronously before returning.
Raises StopAsyncIteration when the limit is reached.
"""
print("-> __anext__ called")
if self.counter >= self.limit:
print("-> Reached limit, raising StopAsyncIteration")
raise StopAsyncIteration
# Simulate a non-blocking I/O wait
await asyncio.sleep(0.5)
value = self.counter
self.counter += 1
print(f" -> Yielding {value}")
return value
async def main():
start_time = time.monotonic()
print("Starting async for loop...")
# The async for loop will use the protocol we defined
async for number in AsyncCounter(3):
print(f"Received {number} in main loop")
duration = time.monotonic() - start_time
print(f"Finished. Total time: {duration:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
Trace the Execution:
- The
async forloop begins and calls__aiter__on theAsyncCounter(3)instance. This returnsself. - The loop then
awaits the first call to__anext__. - Inside
__anext__, the code hitsawait asyncio.sleep(0.5). Execution of__anext__is paused, and control returns to the event loop. - After 0.5 seconds, the event loop resumes
__anext__. It prepares the value0and returns it. - The main loop receives
0and prints it. - This cycle repeats for
1and2. - On the next iteration,
self.counteris3. Theifcondition is met, andStopAsyncIterationis raised, gracefully terminating the loop.
This simple example clearly demonstrates the distinct roles of __aiter__ (setup) and __anext__ (asynchronous work and value production).
Conclusion
In this lesson, we've dissected the formal mechanism that enables asynchronous iteration in Python.
- Key Takeaway: The asynchronous iteration protocol is a direct parallel to the synchronous one, defined by the
__aiter__and__anext__dunder methods. __aiter__: A synchronous method that returns an asynchronous iterator.__anext__: A coroutine method (async def) that is awaited to get the next item. This is where non-blocking waits occur.StopAsyncIteration: The specific exception raised by__anext__to signal the end of the iteration.
You now understand the contract that an object must fulfill to be compatible with the async for statement.
In our next lesson, we will put this knowledge into practice. We will move beyond a simple counter and implement a custom asynchronous iterator class that simulates a more realistic use case: fetching data chunks over a network. This will give you hands-on experience in applying the protocol to solve a common I/O-bound problem.