A triage bot peeks with `head, *rest = fetch_tickets()`, yet every ticket's side effect fires at once and again later — why?
answer
- Ask what the assignment must know first
- Counting leftovers has a price
- Calling it again is not resuming it
- One object, one pass
- Advance the iterator instead of unpacking it
basics
~20 sStarred unpacking is eager: it drains the generator to count the leftovers, so every side effect inside it runs at that line. Calling the generator function again creates a brand-new generator, so the whole stream runs a second time.
solid answer
~50 sTwo separate mistakes compound. First, a starred target has to know how many values remain, so the assignment runs the iterable to exhaustion and materialises the tail as a list — every side effect embedded in the producer fires immediately, and the streaming and memory benefits of a generator are gone. Second, a generator object is single-use; calling `fetch_tickets()` a second time builds a *new* generator that replays the whole sequence, so any per-item side effect happens twice. The fix is to keep one iterator and pull from it: `it = fetch_tickets()`, then `head = next(it)`, and consume the remainder from `it` — or take a bounded prefix with `itertools.islice`, or `itertools.tee` when two passes are genuinely needed. Better still, move the side effect out of the producer so consuming it is repeatable and harmless.
code
python · 8 linesdef fetch_tickets():
for i in range(3):
print("marking ticket", i, "as seen")
yield i
head, *rest = fetch_tickets()
print(head, rest)
# all three markings print before this line runsgo deeper
Recall the two core facts: a starred target consumes the whole iterable, and a generator object can be iterated only once. Calling the generator function a second time starts a new, independent run.
Explain why the assignment must exhaust the source — it has to count the leftovers — and show the replacement: hold one iterator, use next for the head, and keep consuming that same object.
Diagnose it from the symptom. Duplicated per-item effects with no exception point at a producer being run twice; prove it by counting production in a fast test, then choose between a single-pass restructure, a bounded prefix and buffering, naming the memory tradeoff.
Own the rule that producers should be side-effect-free and that any function returning a lazy iterator has made single-consumption part of its contract. Decide where your codebase draws that line, and make the fast test that enforces it a standing gate rather than something a long suite catches late.
### Why the assignment drains the stream `head, *rest = fetch_tickets()` is extended iterable unpacking. To bind `rest`, the interpreter must know how many values are left after the fixed targets are satisfied, and the only way to learn that from an arbitrary iterable is to pull items until it stops. So the assignment iterates the generator to exhaustion and builds a list of everything after the first item. Three consequences follow, and a senior answer names all three: - **Laziness is gone.** The generator was presumably written to stream, so that the bot processes tickets as they arrive. The unpacking line collapses that into one blocking pass. - **Memory is unbounded.** `rest` is a real list holding every remaining item. On a backlog burst it is the whole backlog, resident. - **Side effects fire early and all at once.** If the producer marks each ticket as seen, emits a metric, or writes an audit row as it yields, all of those happen at the assignment, not as each ticket is handled. If the process dies mid-triage, every ticket is already marked but none is triaged. Even the un-starred form consumes more than it looks. `a, b = some_generator()` pulls a *third* item before it can decide the stream was too long, so the side effect for item three has already run when the `ValueError: too many values to unpack (expected 2)` is raised. ### Why the effect then happens twice A generator function returns a fresh generator object each time it is called, and each generator object is single-use: once exhausted it yields nothing further. So code that "peeks" with `head, *rest = fetch_tickets()` and later iterates `for ticket in fetch_tickets():` has called the function twice and produced two independent runs of the same sequence — with the per-item side effect executed twice per ticket. Nothing errors; the duplication is silent, which is why it survives review. The near-miss variant is worse: reusing the *same* exhausted generator object for the second pass produces zero iterations, so the loop body silently never runs. One spelling duplicates work, the other skips it, and both look correct on the page. ### Diagnosing it The symptom is duplicated per-item effects with no exception — double audit rows, a metric counting twice, a webhook delivered twice. Confirm it by counting production, not consumption: wrap the producer in a counter, or assert in a test that a fake source is iterated exactly once. That test must be fast enough to run constantly; if the only coverage lives in a 27-minute suite nobody runs before pushing, the defect reaches production, and duplicated side effects are exactly the class of bug that a slow suite is too late to catch. A focused unit test with a hand-rolled counting iterable runs in milliseconds and pins the contract. ### Fixing it **Keep one iterator and pull from it.** The direct replacement for peeking is `next`: ```python it = fetch_tickets() head = next(it) # or next(it, None) for a safe empty case for ticket in it: # the same iterator, resumed where it stopped triage(ticket) ``` The generator is advanced exactly once for the head and then continues; nothing is materialised and each side effect happens once, at the moment its ticket is handled. **Take a bounded window with `itertools.islice`** when you want the first N without draining the rest. `list(islice(it, 10))` gives ten items and leaves the iterator positioned for the rest. **Use `itertools.tee` only when two genuine passes are needed**, and remember what it costs: it buffers every item the slower consumer has not yet reached, so if one branch runs far ahead the buffer grows to the gap. `tee` trades duplicate *production* for memory; it is right when re-running the producer would repeat side effects, wrong when the sequence is huge and the consumers move at different speeds. **Or remove the reason to care.** The deepest fix is that a producer should not carry the side effect at all. If `fetch_tickets` only yields tickets and the "mark as seen" write lives in the consumer, then consuming it twice is merely wasteful rather than wrong, and unpacking it is a memory question instead of a correctness one. Side effects buried in a generator make every consumption pattern a hazard, and callers cannot see them. **Guard the boundary.** Where a function must return something safely re-iterable, return a concrete sequence and say so in the signature and the docstring, rather than handing back a generator that the caller will unpack and re-call.
- How many items does `a, b = some_generator()` pull before it raises for a three-item stream?Three. The interpreter cannot know the stream is too long until it asks for one more item than the targets need, so it pulls a third value and only then raises `ValueError: too many values to unpack (expected 2)`. Any side effect attached to producing that third item has already happened when the exception surfaces.
- When is `itertools.tee` the right answer, and what does it cost?It is right when you genuinely need two passes over a stream whose producer must not run twice — because production is expensive or has side effects. The cost is memory: `tee` buffers every item the lagging consumer has not yet consumed, so if one branch races ahead the buffer grows to the gap between them. For a large stream with consumers at different speeds, materialising a list or restructuring to a single pass is safer.
- What happens if the second pass reuses the same generator object instead of calling the function again?Nothing runs. An exhausted generator yields no further items, so the loop body executes zero times and no exception is raised. That is the mirror-image defect of the duplicate: one spelling does the work twice, the other silently skips it, and neither announces itself. Both are why a single-use iterator should be consumed in one place.
It is like emptying a mail sack onto the desk just to see the top letter, then asking the courier for the same sack again — you get a second delivery, not the rest of the first.
saying these in an interview costs you the question
- Thinks starred unpacking is lazy over a generator
- Says calling the generator function again resumes it
- Believes naming the target `_` avoids building the list
- Assumes an exhausted generator restarts on the next loop
- Reaches for tee without mentioning its buffering cost
- Leaves side effects inside the producer and calls it fixed