Skip to content

experimental-inspect: Iterator __next__ not optional - #6274

Open
jonasdedden wants to merge 4 commits into
PyO3:mainfrom
jonasdedden:experimental-inspect-next-not-optional
Open

experimental-inspect: Iterator __next__ not optional#6274
jonasdedden wants to merge 4 commits into
PyO3:mainfrom
jonasdedden:experimental-inspect-next-not-optional

Conversation

@jonasdedden

Copy link
Copy Markdown
Contributor

PyO3 lets __next__ be written as -> Option<T>, where None means "raise StopIteration". Introspection resolved that through the ordinary return-type path, so the stub said:

    def __next__(self, /) -> int | None: ...

Which is wrong in a way that costs more than Incomplete would. mypy takes __next__'s return type as the iteration item type, so every for x in it binds int | None, and the class stops satisfying Iterator[int].

IterNextOutput<T> strips the encoding for the two shapes that carry it, Option<T> and Result<Option<T>, E>, using the inherent-const-beats-trait-const trick PyO3 already uses for Probe. An iterator that always yields and raises to stop - PyClassIter in pytests - matches neither and falls through to PyReturnType unchanged.

pytests gains PyClassOptionIter, which had no counterpart: every existing __next__ there returns a plain value, so nothing covered this shape.

jonasdedden and others added 4 commits July 31, 2026 13:48
PyO3 lets `__next__` be written as `-> Option<T>`, where `None` means "raise
`StopIteration`". Introspection resolved that through the ordinary return-type
path, so the stub said:

    def __next__(self, /) -> int | None: ...

Which is wrong in a way that costs more than `Incomplete` would. mypy takes
`__next__`'s return type as the iteration item type, so every `for x in it`
binds `int | None`, and the class stops satisfying `Iterator[int]`. On a real
extension whose iterators are its main output type, exporting the iterator
classes with this bug produced 66 new `mypy --strict` errors, against 0 when
they were left unexported and typed `Any`.

`IterNextOutput<T>` strips the encoding for the two shapes that carry it,
`Option<T>` and `Result<Option<T>, E>`, using the inherent-const-beats-trait-
const trick PyO3 already uses for `Probe`. An iterator that always yields and
raises to stop — `PyClassIter` in `pytests` — matches neither and falls through
to `PyReturnType` unchanged.

`pytests` gains `PyClassOptionIter`, which had no counterpart: every existing
`__next__` there returns a plain value, so nothing covered this shape.
`PyClassOptionIter` returned `PyResult<Option<usize>>` and never errored,
which trips `clippy::unnecessary_wraps` and broke the clippy jobs.

Split the two shapes the introspection change strips instead of picking
one: `PyClassOptionIter` now returns a plain `Option<usize>`, and the new
`PyClassResultOptionIter` keeps `PyResult<Option<usize>>` — the wrapper
is the point there, so the lint is `#[expect]`ed with a reason. Both
stubs come out as `-> int`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcrSbuvXqbWWormVZCv3Tq
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.

1 participant