Python Async Comprehensions — Core Concepts
What Async Comprehensions Are
Python 3.6 introduced two new forms of comprehension syntax via PEP 530:
- Async for comprehensions:
[x async for x in aiter] - Await in comprehensions:
[await f(x) for x in iter]
These combine two powerful Python features — comprehensions and async/await — into concise expressions for building collections from asynchronous sources.
The Two Flavors
Async For Comprehensions
These iterate over an async iterator — an object that produces values one at a time, with potential waiting between each value.
results = [row async for row in db.fetch_rows("SELECT * FROM users")]
The async for handles calling __aiter__ and __anext__ on the async iterator, awaiting each value.
Await in Comprehensions
These call an awaitable function for each item in a regular (synchronous) iterator:
responses = [await fetch(url) for url in url_list]
Here, url_list is a normal list. But fetch(url) returns a coroutine that needs awaiting.
Combining Both
You can mix them:
processed = [await transform(item) async for item in stream if item.valid]
Where You Can Use Them
Async comprehensions work anywhere inside an async def function:
- List comprehensions:
[x async for x in source] - Set comprehensions:
{x async for x in source} - Dict comprehensions:
{k: v async for k, v in source} - Generator expressions:
(x async for x in source)— this creates an async generator
They do not work in regular (non-async) functions. The enclosing function must be async def.
Common Pitfall: Sequential, Not Parallel
The biggest misconception is that [await fetch(url) for url in urls] runs all fetches concurrently. It doesn’t. Each await completes before the next one starts. The items are processed sequentially.
For parallel execution, you need asyncio.gather():
results = await asyncio.gather(*[fetch(url) for url in urls])
Notice the difference: the gather version creates all coroutines first (no await in the comprehension), then waits for them all at once.
When to Use Async Comprehensions
Good fit:
- Consuming an async stream where ordering matters
- Building a collection from sequential async operations
- Filtering async results in a single expression
Bad fit:
- When you need parallel execution (use
gatherorTaskGroup) - When the async iterator produces thousands of items (memory pressure — consider async generators with
async forloops instead)
Common Misconception
“Async comprehensions make my code concurrent.” They don’t add concurrency — they’re syntactic sugar for an async for loop that appends to a list. The concurrency model is exactly the same as writing out the loop by hand. Their benefit is readability, not performance.
One thing to remember: Async comprehensions are a clean syntax for building collections from async sources, but they run sequentially. For parallel execution, you still need gather() or TaskGroup.
See Also
- Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
- Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
- Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
- Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
- Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.