Pagination leaking everywhere: every caller of your API client repeats the
same while page: fetch, extend, page += 1 dance — or worse, someone
"simplifies" it to fetch_all() and the service melts when a tenant has a
million records.
-
Find the traversal that callers keep re-implementing (pages, DB cursors, chunked file reads, retry-and-continue scans).
-
Write it once as a generator. The generator owns the cursor, the stop condition, and nothing else:
from patterns.behavioral.iterator import iterate_pages def articles(self) -> Iterator[str]: return iterate_pages(self._backend.fetch)
-
Return
Iterator[T], notlist[T]. The signature is the promise of laziness; a list return silently repeals it. -
Let callers bound the work with
itertools.islice/ earlybreak— that's the payoff; don't add alimit=parameter that re-implements it. -
Test the laziness, not just the items. Log fetches in the fake backend and assert consuming 7 items touched 2 pages. If laziness is the contract, an eager regression must fail a test.
__iter__written as a generator makes any class iterable in one line — no iterator class.- Compose, don't accumulate:
islice(count(), …),chain, and generator expressions build pipelines where nothing runs until iteration. - A generator that must clean up (close a cursor) should be consumed with
contextlib.closingor wrapped in a context manager — say which in its docstring.
- Iterators exhaust. A generator iterates once; a second
forgets nothing. Return a fresh iterator per call (asarticles()does), and never stash a half-consumed one in shared state. - The container/iterator confusion: the container's
__iter__returns a fresh iterator; the iterator's__iter__returns itself. Swap them and nested loops break mysteriously. - Side effects in generators run late (or never, if the caller stops early). Don't hide commits or releases inside a traversal.
StopIterationescaping a generator body — say, from an unguardednext()call inside it — would silently end the generator; PEP 479 converts that escape into aRuntimeErrorso the bug is loud. Guard innernext()calls with a default orexcept StopIteration.
examples/paginated_client/ applies every
step to an article API client with an observably lazy fetch log:
uv run python -m patterns.behavioral.iterator.examples.paginated_client.main