Skip to content

[PEP 695] Fix incorrect Variance Computation with Polymorphic Methods. #19466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

randolf-scholz
Copy link
Contributor

@randolf-scholz randolf-scholz commented Jul 16, 2025

My idea for fixing it was to replace typ = find_member(member, self_type, self_type) with typ = find_member(member, self_type, plain_self) inside the function infer_variance, where plain_self is the type of self without any type variables.

To be frank, I do not myself 100% understand why it works / if it is safe, but below is my best effort explanation.
Maybe a better solution is to substitute all function variables with UninhabitedType()?
But I am not sure how to do this directly, since the type is only obtained within find_member.


New tests


According to the docstring of find_member_simple:

Find the member type after applying type arguments from 'itype', and binding 'self' to 'subtype'. Return None if member was not found.

Since plain_self is always a supertype of the self type, however it may be parametrized, the typ we get this way should be compatible with the typ we get using the concrete self_type. However, by binding self only to plain_self, it replaces substituted polymorphic variables with Never.

Examples:

class Foo[T]:
    def new[S](self: "Foo[S]", arg: list[S]) -> "Foo[S]": ...

class Bar[T]:
    def new(self, arg: list[T]) -> "Foo[T]": ...

With this patch:

  • Foo.new becomes
    • def [S] (self: tmp_d.Foo[Never], arg: builtins.list[Never]) -> tmp_d.Foo[Never] in typeops.py#L470
    • def (arg: builtins.list[Never]) -> tmp_d.Foo[Never] in subtypes.py#L2211
  • Bar.new becomes def (arg: builtins.list[T`1]) -> tmp_d.Bar[T`1] (✅)

Without this patch:

  • Foo.new becomes
    • def [S] (self: tmp_d.Foo[T`1], arg: builtins.list[T`1]) -> tmp_d.Foo[T`1] in typeops.py#L470 (❌)
    • def (arg: builtins.list[T`1]) -> tmp_d.Foo[T`1] in subtypes.py#L2211 (❌)
  • Bar.new becomes def (arg: builtins.list[T`1]) -> tmp_d.Bar[T`1] (✅)

Another way to think about it is we can generally assume a signature of the form:

class Class[T]:
    def method[S](self: Class[TypeForm[S, T]], arg: TypeForm[S, T]) -> TypeForm[S, T]: ...

Now, given self_type is Class[T], it first solves Class[T] = Class[TypeForm[S, T]] for S inside bind_self, giving us some solution S(T), and then substitutes it giving us some non-polymorphic method

def method(self: Class[T], arg: TypeForm[T]) -> TypeForm[T]

and then drops the first argument, so we get the bound method method(arg: TypeForm[T]) -> TypeForm[T].

By providing the plain_self, the solution we get is S = Never, which solve the problem.

This comment has been minimized.

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.

[PEP 695] Incorrect Variance Computation with Polymorphic Constructor.
2 participants