Skip to content

Conversation

@sterliakov
Copy link
Collaborator

@sterliakov sterliakov commented Oct 25, 2025

Fixes #19839.

Looks like it was relatively easy to do the right way, let me try! When splitting a callable/parameters into args and kwargs, we have the following options:

  • posonly - only goes to *args, required unless has a default. If we encounter such required arg, all previously collected optional args become required (this only happens due to faulty TVT expansion somewhere; probably I should look into that too)
  • kwonly - only goes to **kwargs, required unless has a default
  • pos-or-kw - goes to both
  • vararg - only goes to *args as an Unpack (possibly normalized by tuple constructor)
  • kwargs - only goes to **kwargs and is only used if there are no kwargs with known names, because PEP 728 is not yet implemented, so we have to choose between dict and TypedDict. (thoughts? Maybe it is better to prefer dict with union(kwarg, *kwargs.values()) as value type? Either way I do not consider this question important as PEP728 will be eventually implemented, and we'll have extra_items for our TypedDicts)

Applying these steps to every argument in order, we collect required and optional args and kwargs candidates. Now, the type of **kwargs is a TypedDict if we know any keys, dict[str, KwargType] if we only have something like **kw: str, and dict[str, Never] if no kwargs were found.

The type of *args is union of all prefixes of optional_args concatenated with required_args: all required args must be there, and optional args can only be passed in order. Since it is uncommon to have a function with more than 10-20 args, I think this union is a reasonable solution.

@sterliakov sterliakov marked this pull request as ready for review October 25, 2025 22:19
@github-actions

This comment has been minimized.

@github-actions
Copy link
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

reveal_type(SneakyPrefix(f9, 1, '', 0).args) # N: Revealed type is "tuple[builtins.str, builtins.int]"

reveal_type(Sneaky(f10, 1, '', '').args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int], tuple[builtins.int, Unpack[builtins.tuple[builtins.str, ...]]]]"
reveal_type(SneakyPrefix(f10, 1, '', '').args) # N: Revealed type is "Union[tuple[()], tuple[Unpack[builtins.tuple[builtins.str, ...]]]]"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is correct, but ugly. Would making it "nice" justify complexity increase?

Copy link
Contributor

@Hnasar Hnasar left a comment

Choose a reason for hiding this comment

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

Very nice!

# TODO: assert this is a trivial type, like Any, Never, or object.
return repl

@classmethod
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you make these classmethods? They don't use cls. Staticmethod or plain method seem less surprising.

Copy link
Collaborator Author

@sterliakov sterliakov Oct 31, 2025

Choose a reason for hiding this comment

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

This is nowhere near performance-critical code (we got a crash report on failing assert only a few years later after it was introduced), so it doesn't matter how optimal mypyc-compiled code is, so it's mostly a matter of preference. I prefer using classmethod because it makes later refactoring easier: it allows accessing other methods without extra diff lines and hardcoding the class name (and not regular methods to keep this code externally usable).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AssertionError when using reveal_type on a class-bound ParamSpec.

3 participants