End itertools.chain when an argument fails - #748
Merged
Conversation
`itertools.chain` resolves its arguments lazily, and marked one *started*
before calling `iter()` on it so a non-iterable would not be retried. But it
never marked the chain *done*, so the failure only skipped the bad argument
and consumption carried on into the ones after it:
c = chain([1], 5, [2, 3])
next(c) # 1
next(c) # TypeError: 'int' object is not iterable
next(c) # Monty: 2. CPython: StopIteration
Wrong values with no error at all — the caller sees a chain that quietly
resumed. CPython clears its source on an `iter()` failure, so the arguments
after the bad one are unreachable and the chain is spent for good.
The asymmetry matters and is easy to overfit: an error from a *resolved*
source's `__next__` must NOT end the chain, because CPython keeps that
iterator in place and hands back the same error on the next call. Only the
`iter()` failure is terminal, so the latch goes in `into_py_iter_tracking`
rather than around the whole loop, and a test pins the `__next__` case so a
future tidy-up cannot merge them.
`next` re-borrows the heap around every step that can run user code, and each
one spelled out the same match-or-`unreachable!`; latching `done` would have
added a sixth copy. `chain_mut` names the re-borrow once, taking those 5
copies down to 2 — the two that read rather than write.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
Ending a chain latched `done` but kept `sources` exactly as passed, so a
spent chain that stayed bound pinned every argument until it was itself
destroyed:
src = [1, 2]
c = itertools.chain(src)
list(c) # CPython: src has one referrer. Monty: two.
CPython `Py_CLEAR`s its source on both endings — the last argument being
drained, and an argument that fails `iter()` — so `finish` now names that
one step: latch `done`, take `sources` and `current`, drop both. The
`iter()`-failure path is where it matters most, since the arguments after
the bad one are unreachable and can never be yielded.
The drop happens after the `&mut Chain` borrow is released, because it can
free objects that refer back to the chain; `itertools__gc.py` exercises
exactly that shape (an argument list that holds the chain), and the refcount
fixture pins both endings with their sources named separately, so a
retained argument shows up as a count of 2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
davidhewitt
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes
itertools.chainso that when an argument fails iter(), the chain is permanently exhausted (raising StopIteration afterwards, as CPython does) instead of silently skipping the bad argument and continuing into the next oneExpected behaviour (CPython):
Fix - set
chain_mut(iter, vm).done = trueas we propagate the errorSummary by cubic
End
itertools.chainwhen an argument failsiter()and clear its sources when the chain ends, matching CPython. Previously the chain skipped the bad argument and continued; now it raisesTypeErroronce and is then exhausted (StopIteration), and it releases its arguments so a spent chain does not pin them.donelatch ininto_py_iter_trackingoniter()failure and callfinishto take and dropsourcesandcurrent; later arguments are never reached and subsequentnext()calls returnStopIteration.__next__raises, the chain stays live and surfaces the same error on the next call.iter()failure), including cycles.chain_mutto avoid repeated matches andfinishto centralize chain teardown; no functional change beyond the new latch and releases.Written for commit 085e736. Summary will update on new commits.