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

Asynchronous Iteration with `async for`

Hello! Welcome back to our series on asyncio.

In our last lesson, we did the "heavy lifting" of implementing a custom asynchronous iterator from scratch. We built the NetworkDataStream class, carefully managing its state and adhering to the asynchronous iteration protocol with __aiter__ and __anext__.

Today, we shift our focus from creating to consuming. Our learning outcome is to consume items from an asynchronous iterator using the async for statement. While you've already seen this syntax in our main() function, we will now formally dissect it to understand how it works, why it's necessary, and how it elegantly hides the complexity of the underlying protocol you just learned.

1. The async for Statement: Syntax and Motivation

At first glance, an async for loop looks nearly identical to its synchronous counterpart. The key difference is the async keyword and, more importantly, the expectation of what happens during each iteration.

A standard for loop expects to get the next item from the iterator immediately. An async for loop is designed for situations where getting the next item might be a non-blocking I/O operation—that is, it might require an await.

To get a clear overview of the syntax and its primary motivation, please watch the first two minutes of the following video.

Async for loops in Python

This segment from the mCoding video 'Async for loops in Python' introduces the async for syntax and explains its core purpose: to handle iterations that involve waiting, such as for network I/O.

Watch from the beginning until 02:09. Focus on the distinction made between a regular for loop and an async for loop, particularly the built-in expectation of waiting.

As the video explains, the fundamental purpose of async for is to integrate the concept of await directly into the loop's mechanism. It allows the event loop to run other tasks while the loop is "waiting" for the next item to become available from the asynchronous iterator.

2. How async for Works Under the Hood

The async for statement is syntactic sugar. It provides a clean, high-level interface for a process that would otherwise be manual and verbose. Given your background, you'll appreciate understanding the formal mechanics that this syntax abstracts away.

The async for loop automatically handles the three key parts of the asynchronous iteration protocol we've discussed:

  1. Calling __aiter__() to get the iterator.
  2. Repeatedly calling and awaiting __anext__() to get the next item.
  3. Catching the StopAsyncIteration exception to terminate the loop.

The following segment from the same video provides an excellent breakdown of this process.

Async for loops in Python

This part of the mCoding video details the low-level asynchronous iterator protocol and explicitly shows how the async for loop uses __aiter__ and __anext__.

Watch the segment from 12:55 to 14:16. Pay close attention to the parallel drawn between the synchronous and asynchronous iterator protocols and how async for automates the process.

To make this completely explicit, let's look at how the async for loop from our previous lesson's main function can be "desugared" into its manual equivalent.

High-level async for loop:

# The clean, high-level syntax
async for data_chunk in NetworkDataStream(total_chunks=5):
    print(f"Processing: '{data_chunk}'")

Manual "desugared" equivalent:

# The manual process that `async for` automates
_async_iterable = NetworkDataStream(total_chunks=5)
_async_iterator = _async_iterable.__aiter__()

while True:
    try:
        # 1. Await the next item
        data_chunk = await _async_iterator.__anext__()
    except StopAsyncIteration:
        # 2. Break the loop when the iterator is exhausted
        break
    else:
        # 3. Execute the loop body with the item
        print(f"Processing: '{data_chunk}'")

Seeing this explicit translation makes the value of the async for syntax clear. It correctly and safely manages the iteration, allowing you to focus purely on the logic within the loop body.

3. A Canonical Use Case: Paginated APIs

One of the most common use cases for asynchronous iterators is fetching data from a paginated API. You request the first page, process its items, then use information from the response (like a next_page_url) to request the second page, and so on, until there are no more pages.

This pattern is a natural fit for an async for loop. The following blog post provides a concise example.

Using asynchronous for loops in Python

This article by Quentin Pradet, 'Using asynchronous for loops in Python', demonstrates a very practical application of async for to consume a paginated HTTP API.

Please read the first two sections, 'Python 3.6+' and the code block directly under it. Notice how the async for loop is used to consume the get_docs async generator, which yields documents page by page.

The async for doc in get_docs() line elegantly hides the complexity of fetching each page, waiting for the HTTP response, parsing it, and yielding the individual documents. From the consumer's perspective, you simply have a stream of documents to process.

4. Alternative Consumption Methods

While async for is the most common way to consume an async iterator, it's not the only one. For situations requiring more granular control, Python provides other tools.

  • anext() built-in function: Just as next() pulls the next item from a regular iterator, anext() (introduced in Python 3.10) awaits and returns the next item from an async iterator. This is useful if you need to fetch items one by one manually, perhaps skipping some or handling the first item differently.
  • Asynchronous Comprehensions: You can also use list, set, or dictionary comprehensions with async iterators.

The Real Python article we've referenced before has a good section covering these alternatives.

Asynchronous Iterators and Iterables in Python

This section of the Real Python article, which you've seen before, introduces alternative ways to consume async iterators beyond the standard async for loop.

Read the section titled 'Using Async Iterators With Other Tools'. You don't need to run the code, but focus on the syntax for anext() and for asynchronous list/dictionary comprehensions.

These tools are powerful additions to your toolkit, but for most streaming data consumption, the async for loop remains the most readable and idiomatic choice.

Conclusion

In this lesson, we've focused on the consumer side of the asynchronous iteration protocol.

  • Key Takeaway: The async for loop is the primary mechanism for consuming items from an asynchronous iterator. It is syntactic sugar that cleanly handles the underlying calls to __aiter__ and await __anext__, as well as the StopAsyncIteration exception.
  • Core Concept: Its purpose is to iterate over a sequence where fetching each item may be an I/O-bound operation that requires await, allowing the event loop to remain unblocked.
  • Practicality: This pattern is ubiquitous in asynchronous Python for handling streaming data, such as from web APIs, database cursors, or file transfers.

In the last lesson, we saw the formal, class-based way to create an async iterator. In this lesson, we saw the formal way to consume one. Next, we'll learn about a much more concise and Pythonic way to create async iterators: asynchronous generators, which use async def with yield.

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

Sign up