Recommended resources
The freely available resources this lesson will direct you to.
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.
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:
- Calling
__aiter__()to get the iterator. - Repeatedly calling and awaiting
__anext__()to get the next item. - Catching the
StopAsyncIterationexception to terminate the loop.
The following segment from the same video provides an excellent breakdown of this process.
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 asnext()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 forloop is the primary mechanism for consuming items from an asynchronous iterator. It is syntactic sugar that cleanly handles the underlying calls to__aiter__andawait __anext__, as well as theStopAsyncIterationexception. - 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.