Typing Iterable vs Iterator vs Generator Hints
TL;DR — Annotate a return as Iterable[T] when callers only loop over it (possibly more than once); use Iterator[T] when it is single-pass or callers call next() on it; use Generator[Y, S, R] only when send() values or the generator’s return value are part of the contract. Every generator function is an Iterator, so Iterator[T] is the usual, tidiest return annotation for one.
These three names describe increasing specificity, not interchangeable synonyms. Getting the level right tells callers whether they may iterate twice, whether the object is exhausted after one pass, and whether two-way communication is on the table. All three live in collections.abc; typing.Iterable and friends are deprecated aliases (ruff UP035).
Iterable loops, Iterator is single-pass, Generator adds two-way communication and a return value.iter versus next
An Iterable[T] implements __iter__, which returns a fresh iterator. A list, set, dict, and range are iterables: you can loop over them repeatedly because each for asks for a new iterator. An Iterator[T] implements both __iter__ (returning itself) and __next__, which yields the next item or raises StopIteration. Once an iterator is exhausted, it stays exhausted.
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Iterable, Iterator
def read_lines(source: Iterable[str]) -> Iterator[str]:
for line in source: # only needs __iter__ on the input
stripped = line.strip()
if stripped:
yield stripped # a generator → an Iterator[str]
read_lines accepts an Iterable[str] because it merely loops. It returns an Iterator[str] because the generator it produces is consumed once.
When Iterable is the right return
Return Iterable[T] when you hand back something callers may loop over more than once — typically a concrete container you build eagerly. Promising Iterable (not Iterator) signals “you can re-iterate this.”
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Iterable
def parse_response(payload: dict[str, list[int]]) -> Iterable[int]:
return payload.get("ids", []) # a list: re-iterable, sized
A caller can loop twice over this result. If you had returned Iterator[int], doing so would silently yield nothing the second time.
The single-pass exhaustion gotcha
The most expensive mistake is annotating a one-shot stream as Iterable[T] and re-iterating it. Type checkers cannot catch this — an Iterator is an Iterable, so passing one wherever an Iterable is expected is perfectly valid typing. The second loop just runs zero times.
# Python 3.9+, mypy 1.10 — type-checks clean, still a bug
from collections.abc import Iterable
def summarize(values: Iterable[int]) -> tuple[int, int]:
total = sum(values) # first pass consumes a generator argument
count = sum(1 for _ in values) # second pass: empty if values was an Iterator
return total, count
summarize(x * x for x in range(5)) # count comes back 0
Iterable[int]. The emptiness on the second pass is a runtime property of single-use iterators that the type system does not model. If a function must iterate its argument twice, materialize it first (data = list(values)) or annotate the parameter as a re-iterable Sequence[int] so generator callers are at least a deliberate choice.
Generator[Y, S, R] when send and return matter
Use the full Generator[YieldType, SendType, ReturnType] only when the extra channels are real. Y is what yield produces, S is what send() injects, and R is the value in the generator’s return. When you never call send() and never use the return value, Iterator[T] is the honest, shorter annotation.
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Generator
def running_average() -> Generator[float, float, None]:
total = 0.0
count = 0
value = yield 0.0
while True:
total += value
count += 1
value = yield total / count # receives the next value via send()
avg = running_average()
next(avg) # prime the coroutine
avg.send(10.0) # 10.0
avg.send(20.0) # 15.0
For a plain producer with no send(), prefer Iterator[float]; mypy and pyright both accept a generator function annotated that way. Reserve Generator for coroutine-style two-way flow.
The async trio mirror these exactly
Async code has a parallel set in collections.abc: AsyncIterable[T], AsyncIterator[T], and AsyncGenerator[Y, S]. The same decision applies — annotate an async def that only yields as returning AsyncIterator[T], and reserve AsyncGenerator[Y, S] for when asend() is used. Note AsyncGenerator takes only two parameters, not three, because an async generator cannot return a value.
# Python 3.10+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import AsyncIterator
async def stream_events(source: AsyncIterator[bytes]) -> AsyncIterator[str]:
async for chunk in source: # consumes with async for
yield chunk.decode() # async generator → AsyncIterator[str]
Mixing the sync and async families is a frequent slip: annotating an async def generator with plain Iterator[T] makes mypy report [misc] because the function returns an AsyncGenerator, not an Iterator.
Common mistakes
- Annotating a producer
Generator[T, None, None]out of habit. Verbose and over-specified; mypy acceptsIterator[T]for any generator that only yields. Simplify unlesssend/returnare used. - Returning
Iterator[T]but expecting callers to loop twice. The second loop is empty at runtime; no analyzer error. Returnlist[T]orSequence[T]when re-iteration is part of the contract. - Importing from
typing.from typing import Iteratortriggers ruffUP035on 3.9+; import fromcollections.abc. - Yielding a value from a function typed
-> Iterable[T]and then calling.send()on it.Iterablehas nosend; mypy reports[attr-defined]. UseGeneratorwhensendis needed.
FAQ
Is every generator an Iterator?
Yes. A generator function returns a generator object that implements __iter__ and __next__, so Iterator[T] is always a valid return annotation for one — and usually the clearest.
What does the S in Generator[Y, S, R] do?
It types the value that generator.send(value) injects, which appears as the result of the yield expression inside the generator. If you never call send(), set it to None or drop down to Iterator[Y].