Traverse a collection's elements — possibly lazily, possibly remote — without exposing how the collection stores them. Callers say "next"; the cursor's bookkeeping is someone else's problem.
| Role | Classic (GoF) form | Python form |
|---|---|---|
| Iterator | An object with next()/done() |
Anything with __next__ — in practice, a generator's frame |
| Concrete iterator | A class holding cursor state | The paused generator frame holds it for free |
| Aggregate | createIterator() factory method |
__iter__, usually written as a generator |
| Client | Calls next() in a loop |
for, comprehensions, unpacking — the protocol is the language |
- The iterable's
__iter__returns a fresh iterator (so two loops don't share a cursor). - The iterator's
__next__returns items and raisesStopIterationwhen done; its own__iter__returns itself. - A generator function implements all of it: each
yieldsuspends the frame, and the frame is the cursor state.
The protocol implemented by hand, the way the guide teaches it:
from __future__ import annotations # OddIterator is named before it exists
class OddNumbers: # the aggregate
def __init__(self, maximum: int) -> None:
self.maximum = maximum
def __iter__(self) -> OddIterator:
return OddIterator(self) # fresh cursor per loop
class OddIterator: # the cursor object
def __init__(self, container: OddNumbers) -> None:
self.container = container
self.n = -1 # cursor state, managed by hand
def __next__(self) -> int:
self.n += 2
if self.n > self.container.maximum:
raise StopIteration
return self.n
def __iter__(self) -> OddIterator:
return selfPython absorbed this pattern deeper than any other. The same behavior as a generator is four lines — the cursor class vanishes into the paused frame:
from collections.abc import Iterator
def odd_numbers(maximum: int) -> Iterator[int]:
n = 1
while n <= maximum:
yield n
n += 2What survives as a design move is hiding a non-trivial traversal (pages,
cursors, chunked reads) behind one generator — this module's
iterate_pages.
- Custom or lazy traversal over your own types: write
__iter__as a generator. - Chunked/remote sources (paginated APIs, cursored queries): expose one generator; keep pages out of caller code.
- Hand-writing
__next__— a generator implements the protocol correctly for you; the manual form is for understanding, not production. - Materializing everything into a list "to be safe" — you just deleted the laziness that justified the pattern.
The pattern is the language. Know the manual protocol (it is the machinery underneath); write generators in practice. Guide chapter: python-patterns.guide/gang-of-four/iterator/