backend / python core / tricky questions / 08_generator_exhaustion.md

Generator exhaustion

1 min read source

Generator exhaustion

The gotcha

Generators (and most iterators) are single-pass. Once consumed, iterating again yields nothing — silently. No exception, just empty.

Minimal repro

gen = (x * 2 for x in range(3))

list(gen)   # [0, 2, 4]
list(gen)   # []     ← exhausted, no error

# Same with files:
f = open("data.txt")
for line in f: ...
for line in f: ...   # second pass yields nothing — file pointer at EOF

A subtler variant — using a generator twice in zip / sum / etc.:

gen = (x for x in range(3))
print(sum(gen))   # 3
print(max(gen))   # ValueError: max() arg is an empty sequence

Why it happens

A generator object holds internal state (instruction pointer, frame). Iterating advances that pointer; when the generator returns, it raises StopIteration permanently. No reset method.

Same applies to: zip(), map(), filter(), enumerate(), reversed(), file objects, csv.reader(), dict.items() views once iterated… wait, actually dict.items() returns a view that can be iterated multiple times. Distinguish:

  • Iterators (single-pass): generators, iter(x), zip, map, filter
  • Iterables (re-iterable): list, tuple, dict, set, range, dict views
r = range(3)
list(r); list(r)   # both [0, 1, 2] — range is iterable, not an iterator

How to avoid

If you need multiple passes, materialize into a list:

data = list(some_generator())
print(sum(data))
print(max(data))

Or use itertools.tee to split a single iterator into N independent iterators (memory-buffered):

import itertools
a, b = itertools.tee(some_generator(), 2)

For files, seek(0) to rewind. For database query results, re-execute.

Interview angle

“Why does this code print 3 then crash?” with a generator passed to sum() then max(). Or: “What’s the difference between a list comprehension and a generator expression?” — leading to memory and reusability.