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¶
- Python 3.11+,
pip install mypy pyright. - Async iteration, from Async Context Managers & Iterators.
- The basics of typing coroutines, from typing coroutine functions.
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.
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.
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.
Verification¶
Async iteration annotations are right when:
- Generator functions return
AsyncIterator[T]orAsyncGenerator[T, None], never the item type. - Resource-owning generators return
AsyncGenerator, soaclose()andaclosing()type-check. - Consumers accept
AsyncIterable[T]unless they callanext()oraclose(). @asynccontextmanagerfunctions are annotated as iterators, andastargets 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¶
AsyncIteratoron 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 forraisesTypeError.- Narrow parameter types. Requiring
AsyncGeneratorrejects 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].
Related¶
- Typing Async Code — up to the topic overview.
- Defining async protocols for dependency injection — structural types for async interfaces.
- Asyncio Fundamentals & Event Loop Architecture — the section overview.