Skip to content

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.

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.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@sterliakov
Copy link
Collaborator

Also fixes #18334 and maybe something else.

@randolf-scholz
Copy link
Contributor Author

@sterliakov I added #18334 as a unit test.

This comment has been minimized.

Copy link
Contributor

github-actions bot commented Oct 6, 2025

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

@randolf-scholz
Copy link
Contributor Author

@sterliakov This and a few other small PRs (#19517, #19471, #19449) have been sitting since mid-July, I'm guessing you are rather swamped. Who else should I ask for review?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants