When does a generator function need Generator[int, str, bool] rather than Iterator[int]?
answer
- Three channels, one parameter each
- Out, in, and finally
- The value of the yield expression
- StopIteration carries the third one
- Iterator is the shorthand for consumers
basics
~20 sGenerator's three parameters are the yield type, the send type and the return type. Use the full form only when callers use send() or read the value carried by StopIteration; for a plain for-loop consumer, Iterator[int] says everything true.
solid answer
~40 s`Generator[YieldType, SendType, ReturnType]` describes all three channels of a generator object: what `yield` produces, what `gen.send(x)` accepts (the value the `yield` expression evaluates to inside the body), and what a bare `return value` puts on `StopIteration.value` — which is also what `yield from` evaluates to in a delegating generator. Most generators use only the first channel, so `Iterator[int]` is the idiomatic annotation: `Generator` is a subtype of `Iterator`, and callers who only run a `for` loop should not be forced to read two `None`s. Reach for the full spelling when the generator is genuinely a coroutine-style pipeline that is driven with `send()`, or when a delegating generator's return value matters. Since Python 3.13, PEP 696 defaults let you write `Generator[int]` and get `None` for both other parameters.
code
python · 20 linesfrom collections.abc import Generator, Iterator
def counter(start: int) -> Generator[int, int, str]:
current = start
while current < 10:
step = yield current
current += step if step is not None else 1
return "done"
def squares(n: int) -> Iterator[int]:
for i in range(n):
yield i * i
g = counter(0)
print(next(g), g.send(4), g.send(4))
try:
g.send(4)
except StopIteration as stop:
print(stop.value)
print(list(squares(4)))go deeper
Remember the order: yield type, send type, return type. For a generator that is only looped over, the send and return types are None, and Iterator[int] is the annotation you will see and should write.
Explain the send channel — that the yield expression evaluates to whatever send() passed in — and that the return value rides on StopIteration.value and on the result of a yield from expression.
Argue the API choice: annotating Iterator[T] keeps the signature honest and leaves you free to swap the generator for another iterator later, while the three-parameter form advertises a driving protocol callers must follow.
Decide when a send-driven generator is the right abstraction at all versus a plain class or a queue, and set the codebase convention for how much generator machinery may leak into public signatures.
### Three channels, three parameters A generator object is not just a source of values; it has three distinct data channels, and `collections.abc.Generator[YieldType, SendType, ReturnType]` names one type for each. **YieldType** is what comes out: the type of every `yield` expression's operand, and therefore the type each `next()` call and each loop variable receives. **SendType** is what goes in: the type accepted by `gen.send(value)`, which is also the type of the `yield` expression *as an expression* inside the body. In `step = yield current`, `step` has the SendType. When a generator is driven by a `for` loop or by bare `next()`, that expression is always `None`, which is why `None` is the SendType of ordinary generators. **ReturnType** is what the generator finishes with: the operand of a bare `return value` inside the body. It never appears in the loop. At runtime it lands on the `StopIteration` instance's `value` attribute, and it is what the `yield from` expression evaluates to in a delegating generator. A generator with no `return` statement, or a bare `return`, finishes with `None`. ### Why Iterator[int] is usually the right annotation A function containing `yield` returns a generator object whatever you annotate, so the annotation is a description of the contract offered to callers, not a transformation. If callers only iterate, then `Iterator[int]` is both true and minimal: it promises `__iter__` and `__next__` and says nothing about channels nobody uses. `Generator` is a subtype of `Iterator`, so a checker accepts a generator-producing body under an `Iterator[int]` return annotation without complaint, and callers annotated to accept `Iterable[int]` or `Iterator[int]` keep working if you later replace the generator with a hand-written iterator class or a list comprehension wrapped in `iter()`. That last point is the real argument: annotating `Generator[int, None, None]` for a function nobody sends into leaks an implementation detail into the signature and blocks that refactor. The full form earns its place when the send channel is real — a running-total accumulator, a state machine fed by its driver, a parser you push tokens into — or when a delegating generator's `return` value is part of the protocol, so that `result = yield from inner()` has a meaningful type. ### Async twin and defaults The asynchronous counterpart is `AsyncGenerator[YieldType, SendType]`, and it has only two parameters, because an `async def` containing `yield` cannot `return` a value at all — that is a syntax error. Its consumer-facing shorthand is `AsyncIterator[T]`, exactly parallel to `Iterator[T]`. Python 3.13 gave these aliases PEP 696 type-parameter defaults, so `Generator[int]` now means `Generator[int, None, None]` and `AsyncGenerator[bytes]` means `AsyncGenerator[bytes, None]`. You can confirm it in a 3.14 REPL: `typing.Generator[int]` prints with the two `NoneType` arguments filled in. Before 3.13 the three-argument spelling was mandatory, which is why so much existing code carries `Generator[int, None, None]`; that is history, not a style rule. ### Spelling Since Python 3.9 (PEP 585) prefer `from collections.abc import Generator, Iterator`; `typing.Generator` and `typing.Iterator` are deprecated aliases of the same runtime classes. And remember the runtime does nothing with any of it: annotating a generator function `-> int` does not make it return an `int`, it merely tells a type checker something false. ### Delegation makes the third parameter visible `yield from` is where the ReturnType stops being decorative. In `result = yield from inner()`, the delegating generator forwards every value `inner` yields and, when `inner` finishes, `result` is bound to whatever `inner` returned. If `inner` is annotated `Generator[int, None, str]`, a checker gives `result` the type `str`; if `inner` is annotated `Iterator[int]`, the checker has been told nothing about a return value and will treat `result` as `None`. So the rule is narrower than "use `Iterator` for consumers": use the three-parameter form whenever another generator delegates to yours and reads the result. ### What each parameter looks like in the body Inside the body, YieldType is the type of every expression you write after `yield`. SendType is the type of the `yield` expression *as a value* — the thing on the left of `step = yield current`. ReturnType is the operand of `return`. A generator that only ever appears in a `for` loop has `None` for the second, because `for` drives it with bare `next()` calls, which is equivalent to sending `None`. ### Related shorthands worth knowing `Iterable[int]` is a legitimate return annotation too, and it is even weaker than `Iterator[int]`: it promises only that the caller can loop over the result, leaving you free to return a list, a tuple or a generator later. Choose it when the function is a query and the caller should not care how the values arrive. Prefer `Iterator[int]` when the result really is single-pass, so that a caller who loops twice is warned by the annotation rather than by an empty second loop. Neither choice changes what a `yield`-containing body returns at runtime — that is always a generator object.
- Where does a generator's ReturnType actually surface at runtime?On the `StopIteration` instance raised when the generator finishes: `stop.value` holds whatever the body returned. A `for` loop swallows that exception, so the value is invisible to ordinary consumers. The other place it appears is in a delegating generator, where `result = yield from inner()` evaluates to exactly that returned value — which is the main reason to type it at all.
- How would you annotate an async generator's send channel?`AsyncGenerator[YieldType, SendType]` — two parameters, not three, because an `async def` that contains `yield` cannot return a value; attempting it is a syntax error. If callers only use `async for`, annotate `AsyncIterator[T]` instead. Since Python 3.13, PEP 696 defaults make `AsyncGenerator[bytes]` mean `AsyncGenerator[bytes, None]`.
- Does annotating a function -> Iterator[int] change what calling it returns?No. Any function whose body contains `yield` returns a generator object, and CPython never consults the annotation. The annotation only tells a type checker, and the humans reading the signature, which channels the caller may rely on. That is precisely why the narrower `Iterator[int]` is safe: it under-promises rather than lying.
saying these in an interview costs you the question
- Annotates a generator function as returning its yield type directly
- Says the second Generator parameter is the return type
- Thinks Iterator[int] is invalid for a function containing yield
- Believes the ReturnType is what next() hands back
- Claims the annotation makes the function build a list