Skip to content

Conversation

@randolf-scholz
Copy link
Contributor

@randolf-scholz randolf-scholz commented Dec 15, 2025

Fixes: #20424

Following my comments in #20416, this PR introduces a new helper function as_type that can be used to determine whether a type can be matched to a certain protocol or not.

As an example application, I fix a bug in is_valid_keyword_var_arg that stems from checking

kwargs <: SupportsKeyAndGetItem[str, Any]

This check is too eager, because SupportsKeyAndGetItem is invariant in the key type, it will produce a false positive when kwargs is for instance dict[Literal["foo", "bar"], int]. The correct test is:

Does there exist T <: str so that kwargs <: SupportsKeyAndGetItem[T, Any]

which can be checked with the new helper function.

Updated tests

I updated testLiteralKwargs to test:

  • both good and bad dict argument
  • both good and bad Mapping argument
  • both good and bad SupportsKeyAndGetitem argument

@github-actions

This comment has been minimized.

mypy/maptype.py Outdated
Comment on lines 29 to 30
from mypy.subtypes import is_subtype
from mypy.typeops import get_all_type_vars
Copy link
Contributor Author

Choose a reason for hiding this comment

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

had to put these there due to circular import issues

Copy link
Member

@ilevkivskyi ilevkivskyi Dec 17, 2025

Choose a reason for hiding this comment

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

This function should definitely not be in this module (ideally we shouldn't even have the existing typeops import below, but that is a separate story).

Also the scope of this function is misleadingly broad. It should probably accept a TypeInfo as target (which probably must be a protocol), and then use fill_typevars(...) as a constraint inference target.

Finally, it is worth doing some performance measurements, we don't want any visible slow-down for something that is only needed for rare edge cases.

Copy link
Contributor Author

@randolf-scholz randolf-scholz Dec 17, 2025

Choose a reason for hiding this comment

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

I put it there since the similar map_instance_to_supertype is in that module. Which module would be appropriate?

Regarding the scope, is this due to design philosophy? It seems useful that one could test for instance if something is a Mapping[T, SomeFixedType] for some T <: str.

If we were to use TypeInfo + fill_typevars, how do you suggest passing the necessary extra information that T <: str? I don't really understand what advantage making target a TypeInfo would give here, it seems like it would make things more complicated.

mypy/maptype.py Outdated
Comment on lines 45 to 47
# need to manually include these because solve_constraints ignores them
# apparently
constraints.append(Constraint(tvar, SUBTYPE_OF, tvar.upper_bound))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it intentional that solve_constraints ignores the upper bounds of the tvars it solves for?

@github-actions

This comment has been minimized.

@sterliakov
Copy link
Collaborator

Just FYI, #20435 could also benefit from this helper function to map to Iterable protocol in a sane fashion

@randolf-scholz
Copy link
Contributor Author

randolf-scholz commented Dec 19, 2025

OK, so I renamed the function to solve_as_subtype, dropped the direction argument and moved it to mypy/subtypes.

I ran some minimal benchmarks with hyperfine:

  1. hyperfine 'pytest -k kwargs -n 0'
    master: Time (mean ± σ): 5.836 s ± 0.209 s, Range (min … max): 5.558 s … 6.092 s 10 runs
    PR: Time (mean ± σ): 5.732 s ± 0.176 s, Range (min … max): 5.511 s … 5.991 s 10 runs
  2. hyperfine 'mypy tmp.py --no-incremental --cache-dir=/dev/null || true' (tmp.py holds testLiteralKwargs` unit test)
    master: Time (mean ± σ): 2.724 s ± 0.040 s, Range (min … max): 2.676 s … 2.788 s 10 runs
    PR: Time (mean ± σ): 2.702 s ± 0.038 s, Range (min … max): 2.646 s … 2.791 s 10 runs
  3. hyperfine --warmup 1 'mypy tmp.py --no-incremental --cache-dir=/dev/null || true' (tmp.py holds func(**{'a': 1, 'b': 2}) x 400)
    master: Time (mean ± σ): 2.742 s ± 0.037 s, Range (min … max): 2.679 s … 2.776 s 10 runs
    PR: Time (mean ± σ): 3.009 s ± 0.044 s, Range (min … max): 2.945 s … 3.079 s 10 runs

The first two show no difference (most time likely spent on setup/teardown), the last one show roughly a 10% performance degradation.

@github-actions

This comment has been minimized.

@randolf-scholz
Copy link
Contributor Author

A few more changes:

  • added a fast path for dict, test 3 from my previous comment is now comparably fast to master
  • added a fast fail path if the the argument is not a SupportsKeyAndGetitem.
  • added flavor-check for ParamSpec
  • added factorization over union + test (this was producing false positives on master as well: https://mypy-play.net/?mypy=master&python=3.12&gist=5bd4a2ba27f5d5edec09efb4486f65d9)
  • fixed confusing error message Argument after ** must be a mapping when the argument is a union of mappings.

@github-actions
Copy link
Contributor

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

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.

False positive error on **kwargs with Mapping[Literal, Any] type

3 participants