Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Understanding Asynchronous Iteration: `__aiter__` and `__anext__`

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:

  1. The __iter__ method, which returns an iterator object.
  2. The __next__ method, which returns the next item.
  3. The StopIteration exception, 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.
  • StopIteration becomes StopAsyncIteration.

The following video provides a very clear and concise explanation of this protocol.

Async for loops in Python

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, not async 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 simply return self. This convention makes all asynchronous iterators also asynchronous iterables, allowing them to be used directly in an async for loop. 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 the async for loop to await its 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 StopAsyncIteration exception. Raising the standard StopIteration here would result in a RuntimeError, 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:

  1. The async for loop begins and calls __aiter__ on the AsyncCounter(3) instance. This returns self.
  2. The loop then awaits the first call to __anext__.
  3. Inside __anext__, the code hits await asyncio.sleep(0.5). Execution of __anext__ is paused, and control returns to the event loop.
  4. After 0.5 seconds, the event loop resumes __anext__. It prepares the value 0 and returns it.
  5. The main loop receives 0 and prints it.
  6. This cycle repeats for 1 and 2.
  7. On the next iteration, self.counter is 3. The if condition is met, and StopAsyncIteration is 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.

Can't find a good explanation? Sign up and we'll make it for you

Sign up