Skip to content

fix(inspect): emit parametrized hints for bare container types - #6237

Open
arimu1 wants to merge 3 commits into
PyO3:mainfrom
arimu1:fix-introspection-bare-generics
Open

fix(inspect): emit parametrized hints for bare container types#6237
arimu1 wants to merge 3 commits into
PyO3:mainfrom
arimu1:fix-introspection-bare-generics

Conversation

@arimu1

@arimu1 arimu1 commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Fixes #6236.

experimental-inspect previously emitted bare list, dict, and tuple for standalone Bound<'_, PyList> / PyDict / PyTuple (and Py<...> equivalents), which fails mypy --strict under disallow_any_generics.

This adds STANDALONE_TYPE_HINT on PyTypeInfo / PyTypeCheck, used for direct Bound/Py container types, while keeping TYPE_HINT unparametrized for composition (e.g. Vec<T> still renders as list[T]).

Rust type standalone hint
PyList list[Any]
PyDict dict[Any, Any]
PyTuple tuple[Any, ...]

Test plan

  • PYO3_PYTHON=python3.12 cargo test --features experimental-inspect --lib container_standalone_type_hints
  • PYO3_PYTHON=python3.12 cargo test --features experimental-inspect --lib container_type_hints
  • PYO3_PYTHON=python3.12 cargo build --features experimental-inspect

arimu1 added 3 commits July 25, 2026 08:58
Add STANDALONE_TYPE_HINT for PyList, PyDict, and PyTuple so Bound/Py
forms emit list[Any], dict[Any, Any], and tuple[Any, ...] in generated
stubs while keeping TYPE_HINT unparametrized for composition.

Fixes PyO3#6236
- Rename newsfragments/6236.fixed.md → 6237.fixed.md (PR number)
- Rename #type_hint= → #standalone_type_hint= in pyobject macros
- Fix dict.rs import order; assert Bound/Py INPUT/OUTPUT_TYPE in tests
The introspection change makes bare `dict`, `list` and `tuple` emit
parametrized hints, so the checked-in stubs no longer matched what
`nox -s test-introspection` generates.
@Tpt

Tpt commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Thank you for this MR.

I would tend to prefer we keep the bare container types in the stubs: this mypy lint is to nudge people to actually set a parameter to the collection. In PyO3, it likely implies adding a manual type hint in the signature e.g. #[pyo3(signature = (a: "list["int"]) -> int)]. But glad to be convinced otherwise.

@jonasdedden

jonasdedden commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Thanks @arimu1 for working so quickly on this and thanks @Tpt for already looking into it!

As I created #6236 which this PR is about, let me give you some motivation of why I think it would be beneficial to have this behavior. Ideally, I would want PyO3 to always emit typestubs that are fully mypy --strict and stubtest compliant, and luckily with PyO3 0.29, a fix_stubs.py script of an internal research project already dramatically reduced in scope 🎉

You described a workaround where one could manually set a list[int] typehint. My personal 2 cents on this:

If I have an interface that strictly expect a list of integers, instead of receiving a Bound<'_, PyList>, I would choose to consume a Vec<usize> on the Rust side, if possible. Luckily, to the best of my knowledge and already used in my project, in these cases PyO3 is able to emit typestubs entirely on its own that classify list[int] as a valid type, totally mypy --strict compliant and maximally specific.

In my specific example that occurs a handful of times across the project, I actually want to consume a bare Bound<'_, PyList/PyTuple/PyDict> and specifically, the type of the items of the containers shall be Any. I guess I could also manually set a list[Any] typehint there, but I don't see the additional value of forcing users to have to do this, to be honest, instead of just letting this be the default of PyO3. After all, this is a proper reflection of at least the Bound<'_, PyList> type, which can't be specific to the container item type.

I may come from a bit of an opinionated perspective, but ideally I would want PyO3 to always emit typestubs that are mypy --strict and stubtest compliant per default, without users having to manually write function signatures anywhere.

@jonasdedden

jonasdedden commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Btw., it seems that a bunch of stubs in the pytests directory of the PyO3 repo currently don't seem to be mypy --strict compliant because of this exact issue:

~/workspace/pyo3/pytests main
❯ mypy --strict stubs
stubs/subclassing.pyi:4: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/pyfunctions.pyi:3: error: Function is missing a type annotation for one or more parameters  [no-untyped-def]
stubs/pyfunctions.pyi:3: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/pyfunctions.pyi:3: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/pyfunctions.pyi:28: error: Function is missing a type annotation for one or more parameters  [no-untyped-def]
stubs/pyfunctions.pyi:30: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/pyfunctions.pyi:31: error: Function is missing a type annotation for one or more parameters  [no-untyped-def]
stubs/pyfunctions.pyi:33: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/pyfunctions.pyi:33: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/pyfunctions.pyi:34: error: Function is missing a type annotation for one or more parameters  [no-untyped-def]
stubs/pyfunctions.pyi:36: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/pyclasses.pyi:130: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/pyclasses.pyi:131: error: Function is missing a type annotation for one or more parameters  [no-untyped-def]
stubs/pyclasses.pyi:132: error: Function is missing a type annotation for one or more parameters  [no-untyped-def]
stubs/misc.pyi:5: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/dict_iter.pyi:6: error: Missing type arguments for generic type "dict"  [type-arg]
stubs/datetime.pyi:13: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/datetime.pyi:14: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/datetime.pyi:15: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/datetime.pyi:17: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/datetime.pyi:18: error: Missing type arguments for generic type "tuple"  [type-arg]
stubs/datetime.pyi:19: error: Missing type arguments for generic type "tuple"  [type-arg]
Found 22 errors in 6 files (checked 18 source files)

@arimu1

arimu1 commented Jul 25, 2026

Copy link
Copy Markdown
Author

@jonasdedden good catch — I reproduced your run exactly, and it turns out it's a useful measurement of this PR. Same mypy --strict stubs, same 18 source files:

type-arg no-untyped-def total
merge-base (df72d664) 16 6 22
this branch 2 6 8

So this PR clears 14 of the 16 type-arg errors — every one that comes from a container appearing in a signature. The 8 that survive are two genuinely different generator issues, neither of which this PR touches:

  1. type-arg × 2subclassing.pyi:4 and pyclasses.pyi:130 are class SubDict(dict) / class SubClassWithInit(dict), i.e. #[pyclass(extends = PyDict)] rendering a base class, not a signature type. Different code path.
  2. no-untyped-def × 6def args_kwargs(*args, **kwargs): the varargs are emitted with no annotation at all, so --strict wants *args: Any, **kwargs: Any. Orthogonal to container parametrization.

Both look tractable and I'm happy to do either as a follow-up, but I'd rather not widen this PR while the underlying question is still open.


@Tpt on that question — the argument that persuades me isn't "silence the lint", it's consistency with what PyO3 already does. main already emits bare Any for any scalar parameter it can't infer; there are 31 such annotations across 5 of the existing stub files, e.g.

# pytests/stubs/pyfunctions.pyi on main
def many_keyword_arguments(*, ant: Any | None = None, bear: Any | None = None, ...)

Nobody expects that to nudge the user into hand-writing a signature — it's just the honest rendering of a parameter whose Rust type carries no more information. Bound<'_, PyDict> is exactly the same situation one level down: the Rust type genuinely is "a dict of anything", and there is no element type to recover. So today the generator surrenders to Any at the top level but emits a bare dict inside a container, and only the second one trips --strict.

Put differently: dict[Any, Any] and dict describe the identical type to a type checker — dict just spells it in the form --strict rejects. The nudge you want does still land in the case where it's actionable, because a user who writes Vec<usize> gets list[int] either way; this only changes the case where nothing more specific was ever available.

That said, it's your call and I'm not going to argue it past this. If you'd still prefer bare containers, I'm happy to close this and instead document the #[pyo3(signature = ...)] workaround wherever it best belongs.

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.

experimental-inspect: PyList / PyDict / PyTuple emit bare list / dict / tuple, which fail mypy --strict

3 participants