Skip to content

[Conformance/Spec] Type Checker Divergence in Variance Inference #2281

@randolf-scholz

Description

@randolf-scholz

Currently, all type checkers pass the generics_variance_inference portion of the conformance test suite. However, there is some divergence with generic methods that have bound self-type:
[mypy], [pyright], [ty], [pyrefly]

class Foo[T]:
    def get(self) -> T: ...
    def add[S](self: Foo[S], other: list[S], /) -> Foo[S]: ...

def test_covariance(arg: Foo[int]) -> Foo[object]:
    return arg  # mypy, zuban, ty: ❌️, pyright, pyrefly: ✅️

Following the variance inference specification, I believe Foo should be covariant in T, which is what pyright and pyrefly say, because Foo[Dummy].add is assignable to Foo[object].add. Intuitively, since S is method and not class scoped, and T does not appear in the signature of Foo.add, it has no bearing on variance.

However, mypy, ty and zuban think Foo is invariant in T. In python/mypy#19466 (comment), @ilevkivskyi argued that for an instance, def add[S](self: Foo[S], other: list[S], /) -> Foo[S]: ... is indistinguishable from def add(self, other: list[T], /) -> Foo[T]: ... and therefore Foo should be invariant in T.

This argumentation reveals another divergence between type checkers when it comes to subclassing generics: [mypy], [ty], [pyrefly], [pyright]

class Foo[T]:
    def get(self) -> T: ...
    def add[S](self: Foo[S], other: list[S], /) -> Foo[S]: ...

class Sub(Foo[int]):
    # mypy, zuban, ty, pyrefly: ✅️, pyright: ❌️
    def add(self, other: list[int]) -> Sub: ...

(I believe pyright is correct to error here, as for example a function that expects a Foo[int] could in principle try to call type(arg).add(Foo[str](), ["a", "b", "c"]))

Notably, if we remove the self: Foo[S] annotation, then the inference changes:
[mypy], [pyright], [pyrefly], [ty]

class Foo[T]:
    def get(self) -> T: ...
    def add[S](self, other: list[S], /) -> Foo[S]: ...

def test_covariance(arg: Foo[int]) -> Foo[object]:
    return arg  # mypy, ty, pyright, pyrefly, zuban: ✅️

class Sub(Foo[int]):
    # ty: ✅️ mypy, pyright, zuban, pyrefly: ❌️
    def add(self, other: list[int]) -> Sub: ...

What's needed

Type checker divergence in variance inference is quite insidious; I believe the spec should be clarified in this regard and examples like the ones in this issue should be added to the conformance suite.

I believe that pyright is the only type checker with correct behavior here at the moment, but maybe @erictraut can comment on these examples, and as there is some disagreement, at least from the mypy maintainers, I believe this needs some discussion.

References / Other Discussions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions