Skip to content

Typing Async Iterators and Async Generators

Async generators have three plausible annotations — AsyncIterable[T], AsyncIterator[T] and AsyncGenerator[T, S] — and each one decides what callers may do with the result. The choice matters more than for synchronous generators because closing an async generator is something callers are expected to do, as covered in closing async generators with aclosing. Checked with mypy 2.4.0 (--strict) and pyright 1.1.414: an async generator annotated as returning AsyncIterator[int] could not be closed — aclose() was reported as a missing attribute by both checkers; an async generator annotated as list[int] was rejected by both; an @asynccontextmanager function annotated with the type it yields (-> str) instead of AsyncIterator[str] produced two errors in mypy and four in pyright; and iterating an async iterator with a plain for, or awaiting it, was an error in both. This guide matches each annotation to what callers need.

Prerequisites

1. Annotate async generators by what callers need

An async def that contains yield is an async generator function, and its return annotation describes the object a call produces — not the items:

import asyncio
from collections.abc import AsyncGenerator, AsyncIterator


async def ticks(n: int) -> AsyncIterator[int]:
    for i in range(n):
        await asyncio.sleep(0)
        yield i


async def ticks_gen(n: int) -> AsyncGenerator[int, None]:
    for i in range(n):
        yield i


async def ticks_wrong(n: int) -> list[int]:        # rejected by both checkers
    for i in range(n):
        yield i

mypy rejected ticks_wrong with The return type of an async generator function should be "AsyncGenerator" or one of its supertypes; pyright reported the same at the signature and again at the yield. AsyncIterator[int] is the common, minimal choice: callers can async for over it and call anext(). AsyncGenerator[int, None] additionally promises aclose(), asend() and athrow() — the second parameter is the type accepted by asend(), almost always None.

Verify: every async generator function in the codebase is annotated with AsyncIterator[T], AsyncGenerator[T, None] or AsyncIterable[T] — never the item type or a collection.

What each async iteration type lets callers do 3 stacked layers. What each async iteration type lets callers do AsyncIterable[T] async for only (__aiter__) AsyncIterator[T] + anext(), __anext__ AsyncGenerator[T, S] + aclose(), asend(S), athrow() Parameters should accept the top layer; generator functions that callers must close should return the bottom one.

2. Return AsyncGenerator when callers must close it

The narrower annotation hides methods that callers legitimately need. Measured:

it = ticks(3)                 # annotated AsyncIterator[int]
await it.aclose()
# mypy:    "AsyncIterator[int]" has no attribute "aclose"  [attr-defined]
# pyright: Cannot access attribute "aclose" for class "AsyncIterator[int]"

g = ticks_gen(3)              # annotated AsyncGenerator[int, None]
await g.aclose()              # fine in both
await g.asend(None)           # fine in both

At runtime both objects are async generators and both have aclose(); the annotation decided whether the checker let the caller use it. Code that wraps iteration in contextlib.aclosing() — the reliable way to guarantee cleanup when a consumer stops early — needs the generator type too: aclosing(ticks(1)) was rejected by both checkers (mypy: Value of type variable "_SupportsAcloseT" of "aclosing" cannot be "AsyncIterator[int]"), while aclosing(ticks_gen(1)) passed. A rule of thumb: return AsyncGenerator[T, None] from generator functions whose finally block releases something (a connection, a cursor, a subscription), so callers can close them deterministically; AsyncIterator[T] is fine for pure computations.

Verify: async with aclosing(gen()) as items: type-checks for every generator that owns a resource.

3. Accept AsyncIterable in parameters

A function that consumes async items should accept the widest type, so it works with generators, custom iterator classes and library streams alike:

from collections.abc import AsyncIterable


async def total(source: AsyncIterable[int]) -> int:
    return sum([x async for x in source])

await total(ticks(3))         # AsyncIterator is an AsyncIterable
await total(ticks_gen(3))     # so is AsyncGenerator

Both checkers accepted all three producer types. The same principle as for Awaitable versus Coroutine applies: accept the widest protocol you use, return the narrowest type you guarantee. A consumer that calls anext() directly should accept AsyncIterator[T]; only a consumer that must close the source should demand AsyncGenerator.

Verify: consumer functions take AsyncIterable[T] unless they call anext() or aclose() themselves.

Async iteration mistakes and what the checkers said A grid of 5 rows by 3 columns. Async iteration mistakes and what the checkers said mistake mypy 2.4.0 pyright 1.1.414 async generator annotated list[int] misc error error at def and yield aclose() on AsyncIterator[T] attr-defined attribute error for x in async_gen() not iterable not iterable await async_gen() Incompatible types in await not awaitable @asynccontextmanager ... -> str 2 errors 4 errors Every mistake was caught by both checkers; only the wording differed.

4. Annotate asynccontextmanager functions as iterators

An @asynccontextmanager function is an async generator that yields once. It is annotated with the iterator type, and the decorator turns it into a context manager whose as target has the item type:

from contextlib import asynccontextmanager


@asynccontextmanager
async def connection() -> AsyncIterator[str]:
    conn = "conn"
    try:
        yield conn
    finally:
        pass                  # release the connection


async with connection() as c:
    reveal_type(c)            # str in both checkers


@asynccontextmanager
async def connection_wrong() -> str:      # annotating the yielded value: wrong
    yield "conn"

Measured: connection_wrong produced two mypy errors — the async-generator return type error and Argument 1 to "asynccontextmanager" has incompatible type "Callable[[], str]" — and four in pyright, including No overloads for "asynccontextmanager" match the provided arguments. The correct function revealed c as str. AsyncGenerator[str, None] also works as the annotation; AsyncIterator[str] is the conventional choice because the context manager, not the caller, drives the generator. Patterns for these functions are covered in building async context managers with asynccontextmanager.

Verify: reveal_type of the as target of each async context manager shows the yielded type, not Any.

5. Type async iterator classes and the sync/async boundary

A class implementing the protocol by hand declares __aiter__ returning itself and __anext__ as an async def:

from typing import Self


class Countdown:
    def __init__(self, start: int) -> None:
        self.n = start

    def __aiter__(self) -> Self:          # a plain def, not async def
        return self

    async def __anext__(self) -> int:
        if self.n <= 0:
            raise StopAsyncIteration
        self.n -= 1
        return self.n + 1


async def main() -> None:
    async for x in Countdown(3):          # x: int
        ...
    for x in ticks(3):                    # error: "AsyncIterator[int]" is not iterable
        ...
    await ticks(3)                        # error: not awaitable

__aiter__ must be a regular method; an async def __aiter__ returns a coroutine and fails at runtime with TypeError: 'async for' received an object from __aiter__ that does not implement __anext__: coroutine. Both checkers flagged a synchronous for over an async iterator and an await on one — mistakes that otherwise fail at runtime with TypeError the first time that line runs. The full protocol, including peeking and push-back, is in building async iterator classes with aiter and anext.

Verify: Countdown(3) is accepted wherever AsyncIterator[int] is expected, without casts.

Which async iteration type goes here? A decision on Where does the annotation go with 4 outcomes. Which async iteration type goes here? Where does the annotation go? parameter, iterated with async for AsyncIterable[T] widest parameter, anext() called AsyncIterator[T] needs __anext__ generator owning a resource AsyncGenerator[T, None] callers can aclose() pure generator or asynccontextmanager AsyncIterator[T] conventional Accept AsyncIterable; return AsyncGenerator when callers must be able to close it.

Verification

Async iteration annotations are right when:

  • Generator functions return AsyncIterator[T] or AsyncGenerator[T, None], never the item type.
  • Resource-owning generators return AsyncGenerator, so aclose() and aclosing() type-check.
  • Consumers accept AsyncIterable[T] unless they call anext() or aclose().
  • @asynccontextmanager functions are annotated as iterators, and as targets reveal the yielded type.

Diagnostic Hook: search for @asynccontextmanager followed by a return annotation that is not AsyncIterator[...] or AsyncGenerator[...]; each match makes every async with on that function untyped or an error. Then search for -> AsyncIterator on generators with a finally clause — those are the ones callers will want to close.

Pitfalls & edge cases

  • AsyncIterator on resource-owning generators. Measured: aclose() was an error in both checkers.
  • Annotating the yielded value. On a generator or an @asynccontextmanager, both checkers rejected it.
  • async def __aiter__. It returns a coroutine; async for raises TypeError.
  • Narrow parameter types. Requiring AsyncGenerator rejects custom iterator classes and library streams.

Frequently Asked Questions

Should an async generator return AsyncIterator or AsyncGenerator?

AsyncIterator[T] is enough for callers that only iterate. Use AsyncGenerator[T, None] when callers need aclose() — for generators that own a connection or cursor — because mypy and pyright both rejected aclose() on an AsyncIterator.

How do I type an asynccontextmanager function?

Annotate it as returning AsyncIterator[T], where T is the yielded value; the as target is then typed T. Annotating it as returning T produced two mypy errors and four pyright errors.

What type should a function that consumes an async stream accept?

AsyncIterable[T], which accepts async generators, async iterator classes and library streams; narrow it only if the function calls anext() or aclose().

What is the second type parameter of AsyncGenerator?

The type accepted by asend(). Generators that are only iterated use None, as in AsyncGenerator[int, None].