The Iteration Protocol & Lazy Evaluation
Master Python iteration mechanics: tp_iter and tp_iternext C slots, the StopIteration protocol, custom iterator design, and lazy evaluation pipelines.
The iteration protocol is one of the most fundamental abstractions in Python. By decoupling underlying data storage from sequential traversal state, Python enables lazy evaluation: sequence elements are computed on demand rather than materialized in memory simultaneously.
This architecture allows Python applications to stream gigabytes of logs or infinite numeric series within an invariant memory footprint.
Lazy iterators yield items one at a time, requiring constant memory regardless of stream length.
1. Formal Mathematical Specification of Iteration#
The iteration protocol formalizes the contract between an iterable collection and an active iterator cursor:
Formally, an iterator is a state machine where:
- is the internal iterator state set (e.g. current index or pointer offset).
- is the initial state at instantiation.
- maps current state to updated state and yielded value , or terminal exhaustion state ().
2. C-API Implementation: tp_iter & tp_iternext#
At the C level, Python checks whether an object implements the iteration protocol by inspecting dedicated slots in its PyTypeObject:
Step 1: Acquiring the Iterator via `tp_iter`
When iter(obj) is called, CPython inspects obj->ob_type->tp_iter. If non-NULL, the interpreter invokes this function pointer to allocate and return a new iterator struct.
Step 2: Extracting Values via `tp_iternext`
When next(iterator) or the FOR_ITER bytecode executes, CPython invokes iterator->ob_type->tp_iternext(iterator) directly in C.
Step 3: Handling Value vs Exhaustion
- If
tp_iternextreturns a non-NULLPyObject*, the pointer is pushed onto the frame stack. - If
tp_iternextreturns NULL, CPython inspects thread error state. IfStopIterationwas raised, the loop terminates cleanly; any other error propagates as an unhandled exception.
3. Implementing Custom Iterators#
An object is an iterable if it implements __iter__(). An object is an iterator if it implements both __iter__() (returning self) and __next__():
1234567891011121314151617181920212223242526class PrimeNumberStream: """Infinite iterator yielding prime numbers on demand.""" def __init__(self): self.candidate = 2 def __iter__(self): # An iterator must return itself from __iter__ return self def _is_prime(self, n: int) -> bool: for i in range(2, int(n ** 0.5) + 1): if n % i == 0: return False return True def __next__(self) -> int: while True: current = self.candidate self.candidate += 1 if self._is_prime(current): return current # Lazy consumption: Constant memory! primes = PrimeNumberStream() first_five = [next(primes) for _ in range(5)] print(f"First five primes: {first_five}") # [2, 3, 5, 7, 11]
4. Lazy Evaluation vs Eager Materialization#
Compare the memory consumption of building a complete list versus using a generator iterator:
itertools enable functional stream pipelines. Functions like itertools.islice, itertools.chain, and itertools.groupby process data lazily with zero intermediate list allocations.5. Concept Verification#
Test your understanding of the iteration protocol:
What are the formal requirements for an object to satisfy the Python Iterator Protocol?
What C-API slot does the Python Virtual Machine execute during loop evaluation to fetch the next item from an iterator?