Skip to content

End itertools.chain when an argument fails - #748

Merged
davidhewitt merged 2 commits into
pydantic:mainfrom
rewitt94:fix-chain-source-latch
Aug 17, 2026
Merged

End itertools.chain when an argument fails#748
davidhewitt merged 2 commits into
pydantic:mainfrom
rewitt94:fix-chain-source-latch

Conversation

@rewitt94

@rewitt94 rewitt94 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes itertools.chain so 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 one

Expected behaviour (CPython):

c = chain([1], 5, [2, 3])
next(c) 
next(c) # TypeError
next(c) # StopIteration

Fix - set chain_mut(iter, vm).done = true as we propagate the error


Summary by cubic

End itertools.chain when an argument fails iter() and clear its sources when the chain ends, matching CPython. Previously the chain skipped the bad argument and continued; now it raises TypeError once and is then exhausted (StopIteration), and it releases its arguments so a spent chain does not pin them.

  • Set the chain’s done latch in into_py_iter_tracking on iter() failure and call finish to take and drop sources and current; later arguments are never reached and subsequent next() calls return StopIteration.
  • Preserve the asymmetry: if a resolved source’s __next__ raises, the chain stays live and surfaces the same error on the next call.
  • Add tests for both behaviors and for GC/refcount: a spent chain releases its arguments on both endings (draining the last source and iter() failure), including cycles.
  • Refactor: introduce chain_mut to avoid repeated matches and finish to centralize chain teardown; no functional change beyond the new latch and releases.

Written for commit 085e736. Summary will update on new commits.

Review in cubic

`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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/monty/src/types/itertools/chain.rs Outdated
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 36 untouched benchmarks
⏩ 16 skipped benchmarks1


Comparing rewitt94:fix-chain-source-latch (085e736) with main (edeb82a)

Open in CodSpeed

Footnotes

  1. 16 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

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
davidhewitt merged commit 3c2cd55 into pydantic:main Aug 17, 2026
77 of 79 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants