Python async for vs for: Choosing the Right Loop
python async for vs for: Understand the difference between `async for` and `for` in Python, when each is required, and how to avoid common iteration pitfalls in asynci...
The difference between python async for vs for comes down to the type of iterable you are consuming. A regular for loop works on objects that implement the synchronous iteration protocol (__iter__ and __next__). An async for loop works on async iterables that implement __aiter__ and __anext__, where each step returns an awaitable. Using the wrong loop on the wrong iterable raises a TypeError immediately, so knowing which one your data source requires is not optional.
What async for Changes About Iteration
An async iterable is any object that defines __aiter__() returning an async iterator, and __anext__() returning an awaitable that resolves to the next item or raises StopAsyncIteration when exhausted. The async for statement drives that protocol, awaiting each __anext__() call before executing the loop body.
async def process_stream(stream): async for chunk in stream: await handle(chunk)
Here stream must be an async iterable. The loop internally calls await stream.__anext__() for each iteration, which allows the event loop to run other tasks while waiting for the next chunk to arrive. This is the key difference from a synchronous for, which blocks the thread until the next item is produced.
Async generators are the easiest way to create async iterables. A function that contains yield and is defined with async def becomes an async generator:
async def read_lines(reader): while line := await reader.readline(): yield line
You can then consume it with async for:
async for line in read_lines(reader): print(line)
The async for loop handles the async generator's lifecycle, including calling aclose() when the loop exits early.
Why a Regular for Loop Fails on Async Iterables
A synchronous for loop expects the object to expose __iter__ and __next__. An async iterable has neither; it has __aiter__ and __anext__. Attempting to use for on an async iterable raises:
async def gen(): yield 1 # This raises TypeError: 'async_generator' object is not iterable for item in gen(): print(item)
The same error occurs if you try to call iter() on an async generator. The fix is to switch to async for inside an async def function. The reverse is also true: using async for on a synchronous iterable fails because the object does not implement __aiter__.
This strict separation exists because the two iteration protocols have different execution models. Synchronous iteration runs entirely on the calling thread and blocks between items. Async iteration yields control back to the event loop between items, which is essential for I/O-bound operations but impossible to emulate with a blocking for loop.
When to Use async for vs for
The decision is driven by the data source, not by preference. Use async for when the iterable is asynchronous: an async generator, a network stream, a database cursor that returns rows asynchronously, or any custom object implementing __aiter__. Use for when the iterable is in-memory: a list, tuple, set, dictionary, or a synchronous generator.
# Synchronous iterable: use for items = [1, 2, 3] for item in items: print(item) # Asynchronous iterable: use async for async def async_items(): for i in range(3): await asyncio.sleep(0.1) yield i async def main(): async for item in async_items(): print(item)
A common mistake is to wrap a synchronous iterable in async for just because you are inside an async function. That is unnecessary and will raise a TypeError. Conversely, calling a synchronous function that returns a list and then trying to iterate it with async for is also wrong. The loop type must match the iterable's protocol.
If you have a synchronous iterable that produces values slowly because of I/O, the correct approach is to convert it to an async iterable, for example by wrapping each item in await asyncio.to_thread(...) or by using an async generator that awaits a non-blocking I/O operation. Do not use async for on a plain list.
Performance and Concurrency Considerations
async for does not make your loop concurrent. It awaits each iteration sequentially, meaning the loop body runs to completion before the next item is fetched. The benefit is that while the __anext__() call is pending, the event loop can schedule other tasks. This reduces wall-clock time for I/O-bound workloads because waiting for one item does not block the entire process.
However, if you need to process multiple items concurrently, async for alone is not enough. You must create tasks explicitly:
async def process_all(items): tasks = [asyncio.create_task(handle(item)) for item in items] await asyncio.gather(*tasks)
Here items is a synchronous list, and handle is an async function. This pattern launches all handle calls concurrently. Using async for with an async generator that yields items one by one and then awaiting each handle sequentially would be slower if the operations are independent.
Memory usage also differs. A synchronous for over a large list keeps the entire list in memory. An async generator can yield items lazily, so async for can process a stream without loading everything at once. This is valuable for reading large files, network responses, or database result sets that do not fit in memory.
Common Patterns and Pitfalls
One frequent pitfall is attempting to use async for outside an async def function. The async for statement is only valid inside an async function or an async generator. Running it in a regular function raises a SyntaxError.
Another issue is mixing synchronous and asynchronous code inside the loop body. If the loop body performs blocking I/O, such as time.sleep() or requests.get(), it will block the event loop and negate the benefit of async for. Use await asyncio.sleep() and async HTTP clients instead.
Error handling inside async for works like a normal loop. You can wrap the loop in try/except to catch exceptions raised by __anext__() or the loop body:
async def safe_iterate(stream): try: async for item in stream: await process(item) except StopAsyncIteration: pass except Exception as exc: log_error(exc)
Note that StopAsyncIteration is handled automatically by the loop; you usually do not need to catch it. If you need to break out of an async for loop early, the loop will call aclose() on the async iterator, which allows the generator to run any finally blocks.
Another subtlety: you cannot use yield inside an async for loop directly unless you are inside an async generator. If you need to transform an async iterable into another async iterable, define an async generator and yield from within it.
Choosing the Right Loop for Your Data Source
The table below summarizes the decision based on the iterable type.
| Iterable type | Loop to use | Example | When to use |
|---|---|---|---|
| Synchronous iterable (list, dict, set, sync generator) | for | for x in [1,2,3] | In-memory data, CPU-bound iteration |
| Async iterable (async generator, async stream) | async for | async for x in async_gen() | I/O-bound streaming, lazy data production |
| Synchronous generator that does blocking I/O | Convert to async first | async for x in async_wrapper(sync_gen) | When you must integrate blocking code into asyncio |
| Async iterable that needs concurrent processing | async for + asyncio.gather | async for x in source: tasks.append(create_task(handle(x))) | Independent per-item async operations |
For most application code, the rule is simple: if the object you are iterating over was created by an async def function that uses yield, or if it provides __aiter__, use async for. Otherwise, use for. This rule prevents the most common TypeError and keeps your event loop responsive.
When you control the data source, prefer async generators for lazy streaming and synchronous generators for CPU-bound or small in-memory sequences. If you need to adapt a synchronous iterable to an async context, wrap it in an async generator that awaits a non-blocking operation or uses asyncio.to_thread to avoid blocking the loop.
Finally, remember that async for is a sequential construct. It does not parallelize iteration. For true concurrency, combine it with task creation and asyncio.gather or use higher-level patterns like asyncio.Queue to distribute work across workers. Understanding this distinction will help you write asyncio code that is both correct and efficient.