Back to Blog
Python

Python Iterator Type Hint: Annotate Iterators and Generators

Learn how to use python iterator type hints with typing.Iterator, Iterable, and Generator to improve static analysis and code clarity.

type hintsiteratorsgeneratorstypingstatic analysismypy
A Python code editor showing type hints for an iterator with arrows indicating the flow of iteration.

When you annotate a function that returns an iterator, the correct type hint depends on what the caller can do with the result. Using the wrong hint can cause static type checkers to reject valid code or accept invalid code. The python iterator type hint is not a single syntax; it is a choice among typing.Iterator, typing.Iterable, and typing.Generator, each with a distinct contract.

Iterator vs Iterable: The Type Hint Distinction

The first decision is whether your function returns an iterable or an iterator. An Iterable is any object that can be passed to iter() to produce an iterator. An Iterator is an object that implements __next__() and returns itself from __iter__(). Every iterator is iterable, but not every iterable is an iterator. A list is iterable but not an iterator; calling iter([1, 2, 3]) returns a list iterator.

For type hints, this distinction matters because the caller may need to iterate multiple times. If you annotate a return type as Iterator[int], the caller can only iterate once. If you annotate it as Iterable[int], the caller can iterate repeatedly, but the implementation may return a generator, which is also iterable but single-pass. The hint should reflect the contract you want to guarantee.

from typing import Iterable, Iterator def get_numbers() -> Iterable[int]: return [1, 2, 3] # List is iterable def get_iterator() -> Iterator[int]: return iter([1, 2, 3]) # Explicit iterator

Using Iterable as a return type is safer when the function might return a list, tuple, or generator, because it does not promise a single-pass behavior. Using Iterator signals that the result is a one-shot sequence.

Annotating Return Values with typing.Iterator

When a function returns a generator expression or a generator function, the natural type is Iterator unless you need to specify the generator protocol. For example, a function that reads lines from a file and yields them is best annotated as Iterator[str]:

from typing import Iterator def read_lines(path: str) -> Iterator[str]: with open(path) as f: for line in f: yield line.strip()

This tells static analyzers that the return value supports next() and can be used in a for loop. It also warns callers that the iterator is exhausted after one pass. If you mistakenly annotate the return as List[str], you force the function to materialize the entire file into memory, which defeats the purpose of using a generator.

Using typing.Iterable for Parameters and Inputs

For function parameters, prefer Iterable over Iterator when the function only needs to iterate over the input and does not need to call next() directly. This allows callers to pass lists, tuples, sets, or generators without unnecessary restrictions.

from typing import Iterable, TypeVar T = TypeVar('T') def first_or_none(items: Iterable[T]) -> T | None: for item in items: return item return None

If you annotate the parameter as Iterator[T], callers cannot pass a list without wrapping it in iter(), which is an unnecessary burden. The Iterable hint is more flexible and reflects the actual requirement: the function only needs to iterate once.

A common mistake is to use List[T] for parameters that accept any iterable. That forces callers to convert a generator to a list, which can be expensive for large sequences. Using Iterable[T] avoids that conversion and keeps the function generic.

Type Hinting Generators and Generator Expressions

Generator functions have a more specific type: Generator[YieldType, SendType, ReturnType]. The first parameter is the type of values yielded, the second is the type of values sent via send(), and the third is the type of the return value. Most generators do not use send() or a return value, so the second and third types are often None.

from typing import Generator def countdown(n: int) -> Generator[int, None, None]: while n > 0: yield n n -= 1

If you only need to iterate over the generator, annotating the return as Iterator[int] is simpler and sufficient. However, if the generator accepts values via send() or returns a value after exhaustion, you must use Generator to capture that behavior. For example, a generator that accumulates a sum and returns it:

from typing import Generator def accumulator() -> Generator[int, int, int]: total = 0 while True: value = yield total if value is None: break total += value return total

Here the second type parameter is int because send() expects an integer, and the third is int because the generator returns a total. Static type checkers use these types to validate send() calls and the StopIteration value.

Generator expressions, like (x**2 for x in range(10)), have the same type as a generator function. You can annotate them explicitly, though it is rarely needed because the expression itself infers a type.

Common Mistakes with Iterator Type Hints

One frequent error is using Iterator when Iterable is the correct contract. For instance, a function that takes a list and returns a reversed view should be annotated as Iterable because the caller may want to iterate multiple times. Another mistake is using List for parameters that accept any iterable, which forces unnecessary materialization.

A subtler issue arises with custom iterator classes. If you implement __iter__ and __next__, you must annotate the class correctly. The __iter__ method should return Iterator[T] (or Self in Python 3.11+), and __next__ should return T or raise StopIteration.

from typing import Iterator, TypeVar T = TypeVar('T') class Counter: def __init__(self, limit: int) -> None: self.limit = limit self.current = 0 def __iter__(self) -> Iterator[int]: return self def __next__(self) -> int: if self.current >= self.limit: raise StopIteration self.current += 1 return self.current

If you forget the return type on __iter__, static checkers will infer Iterator[Any] and lose type safety.

Runtime Behavior: Type Hints Do Not Change Execution

Type hints are evaluated at runtime only to populate __annotations__; they are not enforced by the interpreter. This means that annotating a function with Iterator[int] does not prevent it from returning a list. The annotation is documentation for static analyzers and human readers, not a runtime guard. This has practical implications: you can use type hints freely without worrying about performance overhead. The only cost is the import of the typing module, which is negligible.

Because type hints are ignored at runtime, you cannot rely on them to validate input. If you need runtime validation, use isinstance() checks or a library like Pydantic. The type hint is a compile-time contract that is checked only by external tools such as mypy, Pyright, or pytype.

Static Analysis and Compatibility with mypy and Pyright

Mypy and Pyright both understand the typing.Iterator and typing.Iterable abstractions. They use these hints to infer types in loops and comprehensions. For example, given a function returning Iterator[int], a for loop variable is inferred as int. This allows you to catch type mismatches early.

One compatibility note: the typing module is deprecated in favor of collections.abc for runtime checks, but for type hints both are accepted. In Python 3.9+, you can use collections.abc.Iterator and collections.abc.Iterable directly in annotations. For example:

from collections.abc import Iterable, Iterator def process(items: Iterable[int]) -> Iterator[str]: for item in items: yield str(item)

This is equivalent to using typing.Iterator and typing.Iterable and is preferred in modern code because it avoids the deprecated typing aliases. However, if you need to support Python 3.8 or earlier, stick with typing.

Custom Iterator Classes and Protocol

When you define a custom iterator, you should implement the Iterator protocol. The type hint for __iter__ should be Iterator[T] or Self (Python 3.11+). Using Self is more precise because it indicates that the iterator returns itself, which is the standard behavior.

from typing import Self class Squares: def __init__(self, n: int) -> None: self.n = n self.i = 0 def __iter__(self) -> Self: return self def __next__(self) -> int: if self.i >= self.n: raise StopIteration self.i += 1 return (self.i - 1) ** 2

If you use Iterator[int] instead of Self, the type is still correct, but Self is more flexible if the class is subclassed. Static analyzers recognize both.

Another edge case is a class that is iterable but not an iterator, meaning it implements __iter__ but not __next__. In that case, __iter__ should return Iterator[T], typically by returning iter(self.items) or a generator. The class itself is not an iterator, so its type hint should be Iterable[T], not Iterator[T].

from collections.abc import Iterable, Iterator class NumberRange: def __init__(self, start: int, end: int) -> None: self.start = start self.end = end def __iter__(self) -> Iterator[int]: return iter(range(self.start, self.end))

Here NumberRange is iterable but not an iterator. The __iter__ method returns a fresh iterator each time, allowing multiple passes. The type hint Iterable[int] on the class is implicit, but you can add it if you want to be explicit.

Choosing the correct python iterator type hint is about matching the contract you want to expose. Use Iterable when the consumer may need to iterate multiple times. Use Iterator when the result is single-pass. Use Generator only when you need to express send() or a return value. These distinctions keep your code honest and help static analysis catch real bugs.

python iterator type hint: Practical Usage and Code Examples | RYUSLOG DEV