Mastery
Mastery/Python/B. Iteration & generators
T1 · high-leverage

The iterator protocol: why for calls iter() exactly once

An iterable implements __iter__ (returns an iterator). An iterator implements both __iter__ (returning itself) and __next__ (returns the next value or raises StopIteration). for x in obj desugars to roughly: it = iter(obj); while True: try: x = next(it) except StopIteration: break. Understanding this is what lets you read (and write) any custom iterable class in a codebase without treating it as magic.

python
class CountUp:
    """Iterable: a fresh iterator each time -> can be iterated multiple times independently."""
    def __init__(self, n):
        self.n = n
    def __iter__(self):
        return CountUpIterator(self.n)

class CountUpIterator:
    def __init__(self, n):
        self.n = n
        self.i = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i >= self.n:
            raise StopIteration
        self.i += 1
        return self.i

c = CountUp(3)
print(list(c), list(c))   # both calls produce a fresh iterator -> both full runs

it = iter(c)
print(next(it), next(it), next(it))
try:
    next(it)
except StopIteration:
    print("StopIteration after exhaustion, as expected")

A plain iterator object (not a fresh-iterable factory) gets consumed: iterating it twice the second time yields nothing, which is a common source of 'why is my data empty the second time I loop over it' bugs.

python
numbers = iter([1, 2, 3])
print("first pass: ", list(numbers))
print("second pass:", list(numbers), " <- already exhausted, iterator has no reset")

Interview angle

Frequently tested through a "implement a custom iterable class" exercise, which quietly checks several things at once: does the candidate split __iter__/__next__ correctly, do they remember to raise StopIteration, and — the detail most people miss — does their __iter__ return a fresh iterator (so the object can be looped over more than once) or self (which exhausts after one pass)? That last distinction is a great signal of real depth versus surface familiarity.

In the industry

Almost no production code hand-writes iterator classes anymore — generators (yield) are the idiomatic, far more common way to implement custom iteration — but understanding the underlying protocol is essential for reading library internals (how itertools, database cursors, or file objects behave) and for debugging the recurring "why can I only loop over this once" bug, which is almost always an exhausted iterator being mistaken for a reusable iterable.