Back to Blog
Python

How to Write Python Generator Type Hints

Learn to write correct python generator type hints: the three Generator parameters, when Iterator suffices, and how type checkers verify your code.

generatorstype hintstypingmypystatic analysis
Stylized conveyor belt illustration showing typed tokens flowing from a Python generator, representing type-hinted lazily yielded values

Writing a python generator type hint is different from annotating a normal function. A generator function contains at least one yield statement, and calling it does not execute the body immediately. Instead, it returns a generator object that produces values lazily. The type hint must describe that object, not a list or a single value.

def count_up_to(limit: int) -> list[int]: current = 1 while current <= limit: yield current current += 1

This annotation is wrong. The function never returns a list. Calling count_up_to(3) produces a generator object, and a type checker will flag the mismatch because the body contains yield but the declared return type is list[int].

The correct annotation uses the Generator type:

from typing import Generator def count_up_to(limit: int) -> Generator[int, None, None]: current = 1 while current <= limit: yield current current += 1

Generator is a generic type with three parameters. The first is the type of values yielded, the second is the type of values that can be sent into the generator via .send(), and the third is the type of the value returned when the generator finishes. For a plain generator that only yields values, the send and return types are both None.

The Three Parameters of Generator

The full signature is Generator[YieldType, SendType, ReturnType]. Each parameter serves a distinct role:

ParameterMeaningTypical value
YieldTypeType of each value produced by yieldint, str, SomeModel
SendTypeType of values accepted by .send()None if you never send
ReturnTypeType of the value from return when exhaustedNone if no return value

If a generator yields integers and never accepts sent values, Generator[int, None, None] is correct. If it yields strings and returns a final summary value, the third parameter changes:

from typing import Generator def read_lines() -> Generator[str, None, int]: count = 0 for line in open("data.txt"): yield line.strip() count += 1 return count

Here the generator yields each stripped line, and when the file is exhausted it returns the total line count. A caller that exhausts the generator with a for loop will not see the return value; only an explicit next() loop or yield from surfaces it. The type hint records that behavior so callers know what to expect.

When Iterator or Iterable Is the Better Hint

Most generators in real code only yield values. They never use .send(), and they never return a meaningful value. In that case, Generator[T, None, None] is verbose. The Iterator type is a simpler and equally accurate annotation:

from collections.abc import Iterator def count_up_to(limit: int) -> Iterator[int]: current = 1 while current <= limit: yield current current += 1

Iterator[T] is compatible with any generator that yields T and has no meaningful send or return behavior. It is also what for loops and most standard-library functions actually require. If you only need to iterate over the result once, Iterator[int] communicates the contract clearly.

Iterable[T] is even more general. It describes any object that can be iterated, including lists, tuples, and generators. If a function accepts an iterable, annotating the parameter as Iterable[T] is usually better than Iterator[T] because it allows callers to pass lists or sets. But as a return annotation for a generator function, Iterable[T] is less precise: it hides the fact that the result is single-use. Prefer Iterator[T] for generator return types and Iterable[T] for parameters that accept any sequence-like input.

Typing Generators That Accept send() Values

The middle parameter of Generator matters when the generator uses yield as an expression. A generator can receive values from the caller through .send(), and those values become the result of the yield expression:

from typing import Generator def accumulator() -> Generator[float, float, None]: total = 0.0 while True: value = yield total total += value

Here the generator yields the current total and accepts a new value to add. The annotation Generator[float, float, None] says: yields floats, accepts floats via .send(), and returns nothing. A caller can then do:

acc = accumulator() next(acc) # start the generator, get initial total acc.send(2.5) # send 2.5, receive the new total

Without the middle parameter set correctly, a type checker cannot verify that send() calls pass the right type. This is the main case where you need the full three-parameter form rather than Iterator[T].

How Type Checkers Infer Generator Types

Mypy, Pyright, and other type checkers infer the yield type from the yield statements in the body. If you annotate the function with Generator[int, None, None] but the body yields strings, the checker reports a mismatch. The same applies to the return type: if the body contains return 42, the third parameter must be int.

Inference also works in the other direction. If you write a generator function without an explicit return annotation, the checker infers a Generator type from the body. That inferred type is often more specific than you need, which is why an explicit Iterator[T] annotation is useful: it narrows the public contract and prevents callers from depending on send or return behavior you did not intend to expose.

One practical detail: yield from changes what the checker verifies. If a generator delegates to another generator with yield from, the delegated generator's yield type must match the outer generator's yield type. The send and return types must also be compatible, because yield from forwards both.

Common Mistakes in Generator Type Hints

A frequent mistake is using List[T] or Sequence[T] as the return annotation for a generator function. The function does not return a list, so the annotation is false and the checker will reject the body.

Another mistake is forgetting the third parameter when the generator returns a value. Generator[int, None] is invalid; Generator always takes three type arguments. If you only care about the yield type, use Iterator[int] instead of trying to shorten Generator.

A subtler issue: annotating a generator with Generator[int, None, int] when the body has no return statement. The checker will complain that the declared return type is int but the function can complete without returning a value. Either add the return or change the third parameter to None.

Mismatches also appear when the same generator is used in multiple contexts. If one consumer treats it as Iterator[str] and another uses send() with an int, the annotations cannot both be correct. The public type hint should reflect the full behavior, not the easiest single use.

Version Compatibility and Import Choices

The typing module has provided Generator since Python 3.5. If you need to support older Python versions, from typing import Generator is the safe choice. On Python 3.9 and later, collections.abc exposes the same generic types directly:

from collections.abc import Generator def count_up_to(limit: int) -> Generator[int, None, None]: ...

collections.abc.Generator and typing.Generator are aliases for the same runtime type. The collections.abc import is preferred in modern code because the typing versions are deprecated for this purpose in newer Python releases. The same applies to Iterator and Iterable: prefer collections.abc on Python 3.9+.

There is no runtime cost to any of these annotations. Type hints are evaluated lazily unless you use from __future__ import annotations, and they do not affect generator execution. The only requirement is that the annotation is correct, because type checkers and IDE tooling rely on it for autocompletion, refactoring, and static validation.

python generator type hint: Practical Usage and Code Example | RYUSLOG DEV