skip to content

When does a generator function need Generator[int, str, bool] rather than Iterator[int]?

level: middleimportance: should knowfreq 44%

answer

  1. Three channels, one parameter each
  2. Out, in, and finally
  3. The value of the yield expression
  4. StopIteration carries the third one
  5. Iterator is the shorthand for consumers

basics

~20 s

Generator'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 lines
python
from 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context