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 gather or TaskGroup)
  • When the async iterator produces thousands of items (memory pressure — consider async generators with async for loops 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.

pythonasynccomprehensions

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.