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.