Python Async Iterators — Core Concepts

The Async Iteration Protocol

Just as regular iterators implement __iter__ and __next__, async iterators implement two special methods:

  • __aiter__ — returns the async iterator object (usually self)
  • __anext__ — returns an awaitable that resolves to the next value, or raises StopAsyncIteration when exhausted
class Countdown:
    def __init__(self, start):
        self.current = start
    
    def __aiter__(self):
        return self
    
    async def __anext__(self):
        if self.current <= 0:
            raise StopAsyncIteration
        self.current -= 1
        await asyncio.sleep(1)  # Simulate waiting
        return self.current + 1

Usage with async for:

async for number in Countdown(5):
    print(number)  # Prints 5, 4, 3, 2, 1 — one per second

async for Under the Hood

When Python encounters async for item in source, it does:

  1. Calls source.__aiter__() to get the iterator
  2. Repeatedly calls await iterator.__anext__() to get values
  3. Assigns each value to item and runs the loop body
  4. Stops when StopAsyncIteration is raised

The crucial difference from regular for: step 2 uses await, meaning the event loop can run other tasks while waiting for the next value.

Async Generators: The Easy Way

Writing a class with __aiter__ and __anext__ is verbose. Python 3.6+ provides async generators — functions that use both async def and yield:

async def fetch_pages(urls):
    async with aiohttp.ClientSession() as session:
        for url in urls:
            async with session.get(url) as response:
                yield await response.text()

This creates an async iterator automatically. No class needed.

async for page_html in fetch_pages(url_list):
    process(page_html)

When to Use Async Iterators

Streaming data: When data arrives piece by piece — WebSocket messages, server-sent events, paginated API results.

Large datasets: Instead of loading everything into memory, process rows one at a time:

async def query_large_table(conn):
    async for row in conn.cursor("SELECT * FROM huge_table"):
        yield transform(row)

Event-driven patterns: Listening for events indefinitely:

async def event_listener(queue):
    while True:
        event = await queue.get()
        if event is None:
            break
        yield event

Async Iterators vs Regular Iterators

FeatureRegular IteratorAsync Iterator
Protocol__iter__, __next____aiter__, __anext__
Loop syntaxfor x in iterasync for x in aiter
Can awaitNoYes
Stop signalStopIterationStopAsyncIteration
Works in sync codeYesNo — needs async def context

Common Misconception

“Async iterators process items in parallel.” They don’t. Each __anext__ call is awaited sequentially — you get one item, process it, then get the next. The benefit isn’t parallelism; it’s non-blocking waiting. While one async iterator waits for its next item, the event loop can service other coroutines.

If you need to consume items from multiple async iterators simultaneously, you’d use asyncio.TaskGroup or libraries like aiostream.

One thing to remember: Async iterators implement __aiter__ and __anext__ to deliver items with non-blocking waits between them. Use async generators (async def + yield) for the simplest way to create them.

pythonasynciterators

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.