Why does a second for loop over the same generator object produce nothing?
answer
- The second loop sees an empty source
- Ask what iter() gives back each time
- One cursor, and it only moves forward
- StopIteration means done, not error
- No rewind: rebuild, materialise or tee
basics
~20 sA generator object is its own iterator and holds one position. Once it has raised StopIteration it is exhausted for good, so a second for loop starts at the end and finishes immediately, silently and without an error.
solid answer
~40 sCalling `iter()` on a generator object returns that same object, so there is only ever one cursor into it. The first loop advances that cursor to the end; from then on every `next()` raises `StopIteration` again, and a `for` loop treats that as "done" rather than an error, so the second loop body simply never runs. A list behaves differently because `iter(a_list)` hands back a **fresh** iterator each time, which is why lists, tuples, `range` objects and dict views can be looped repeatedly. Generators cannot be rewound or reset. If you need the values twice, either materialise them once with `list(...)`, call the generator function again to get a new generator object, or hand callers a factory object whose `__iter__` builds a fresh generator per loop.
code
python · 9 linesdef flag_names(rows):
for row in rows:
yield row.strip()
gen = flag_names([" beta_ui ", "new_pricing"])
print(list(gen)) # ['beta_ui', 'new_pricing']
print(list(gen)) # []
print(sum(1 for _ in gen)) # 0
print(iter(gen) is gen) # True -- one shared cursorgo deeper
Recall the headline: a generator object can be looped once. Be able to show the two-loop snippet, say that the second loop prints nothing and raises nothing, and name list() as the simplest fix when you need the values twice.
Explain the mechanics: iter(gen) is gen, so one cursor is shared; once the body returns, StopIteration repeats forever and the frame is gone. Contrast that with iter(a_list) returning a fresh iterator on every call.
Show how you keep this out of production: honest return annotations, an Iterator check plus list() at API boundaries that need two passes, and a regression test that iterates a returned value twice and asserts equal results.
Own the contract question. Decide as a matter of house style whether internal functions may return one-shot iterators at all, where the materialise-or-stay-lazy boundary sits given memory budgets, and how that expectation is expressed in types and reviewed.
## The symptom The bug looks like data loss. A function builds a generator object, one piece of code loops over it to count or validate, a second piece of code loops over it to do the real work, and the second loop sees nothing at all. No exception, no warning, no traceback — just an empty result where a full one was expected. ```python def flag_names(rows): for row in rows: yield row.strip() gen = flag_names([" beta_ui ", "new_pricing"]) print(sum(1 for _ in gen)) # 2 print(list(gen)) # [] -- the values are gone ``` ## Why it happens A `for` loop begins by calling `iter()` on whatever it is given. For a list, `iter()` constructs a brand-new `list_iterator` positioned at index 0, so every loop is independent. For a generator object, `iter()` returns *the generator object itself* — a generator is its own iterator, and `iter(gen) is gen` is `True`. There is therefore exactly one cursor, shared by every consumer, and it only moves forward. Each `next()` resumes the suspended function body until the next `yield`. When the body finally returns, `next()` raises `StopIteration`, and the generator marks itself finished and drops its execution frame. Every subsequent `next()` raises `StopIteration` immediately. There is no `reset()`, no `seek(0)`, no way back: the state that would let it replay the values no longer exists. ## Why it is silent `StopIteration` is the normal end-of-iteration signal, not an error. `for` catches it and exits cleanly, so a loop over an exhausted generator is indistinguishable from a loop over an empty source. The consequences are quiet and wrong rather than loud: `sum()` returns `0`, `any()` returns `False`, `all()` returns `True`, `list()` returns `[]`, `"x" in gen` returns `False`, and `len(list(gen))` reports zero items where the data really did exist a moment earlier. `max()` and `min()` are the only common builtins that complain, and only because an empty sequence has no maximum. The patterns that trigger it in real code are always the same shape: count first and then process; validate first and then write; log a preview and then send; or simply pass the same generator object to two helper functions. ## The four ways out **1. Materialise it.** If the data is bounded and small enough to hold, `values = list(gen)` once, then reuse `values` freely. This is the right default for hundreds or thousands of small records; it trades memory for a container that can be measured, indexed and looped as often as you like. **2. Rebuild it.** Call the generator function again. `flag_names(rows)` is cheap; each call returns a new generator object with a fresh frame. This works only if the underlying source is itself re-readable — rebuilding over an already-drained iterator just produces another empty generator. **3. Hand out a factory, not an iterator.** Return an object that builds a new generator on every `__iter__` call. Callers then get list-like "loop me as often as you want" semantics while the values stay lazy: ```python class FlagNames: def __init__(self, rows): self._rows = rows def __iter__(self): return (row.strip() for row in self._rows) ``` **4. `itertools.tee`.** When two consumers genuinely need the same live stream, `tee` splits one iterator into independent branches — at the price of buffering whatever the leading branch has consumed and the lagging one has not. Useful for two consumers running roughly in step; a poor substitute for `list()` otherwise, and after teeing you must stop touching the original iterator. ## Defending against it Because the failure is silent, the cheap protection is to make the contract explicit. Annotate a return type as `list[str]` when you promise re-iteration and as `Iterator[str]` when you do not, name the parameter honestly, and at an API boundary that must survive being looped twice, check `isinstance(value, collections.abc.Iterator)` and materialise. Checking for `Iterable` proves nothing — every iterator is also an iterable; `Iterator` is the check that says "this argument is one-shot". And write the test that iterates the result twice: that single test turns an invisible data-loss bug into a red build.
- How can you tell, before consuming it, whether an argument is a one-shot iterator or a re-iterable container?Check `isinstance(value, collections.abc.Iterator)`, or equivalently `iter(value) is value`. Both are true exactly for objects that are their own iterator — generators, `map`, `filter`, `zip`, `enumerate`, file objects. Checking `collections.abc.Iterable` proves nothing, because every iterator is also an iterable. If the check says iterator and you need more than one pass, materialise with `list()` at the boundary.
- What happens if you call next() on a generator object that has already raised StopIteration?It raises `StopIteration` again, immediately, forever. When the generator body returns, the generator drops its frame and marks itself finished, so there is no state left to resume. Nothing resets it — you have to call the generator function again to get a new generator object, and that only helps if the underlying source can also be read again.
- Why is this bug so often reported as missing data rather than as a crash?Because exhaustion is not an error condition. `StopIteration` is the protocol's normal end signal, so `for`, `list()`, `sum()` and `any()` all treat an exhausted generator exactly as they treat an empty one: zero items, no complaint. The wrong answer flows downstream unchallenged, which is why an explicit second-iteration test is worth writing.
A list is a book you can reopen at page one; a generator object is a live ticker tape — once it has run through the reader, there is nothing left to read again.
saying these in an interview costs you the question
- Says a generator restarts from the beginning on a new for loop
- Claims the second loop raises StopIteration to the caller
- Thinks calling iter() on a generator gives a fresh cursor
- Believes a generator can be reset or rewound
- Assumes an empty result must mean the source was empty
- Uses isinstance against Iterable to detect a one-shot argument