[Conformance/Spec] Type Checker Divergence in Variance Inference
Update: Since v0.0.36 ty now also considers Foo covariant in T in the example below
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
- [PEP 695] Incorrect Variance Computation with Polymorphic Constructor. mypy#19439
- Generic method in generic class impacting type signature of unrelated method mypy#20644
- generic is incorrectly inferred as invariant if class uses
Selftype on an attribute mypy#18334 - [PEP 695] Fix incorrect Variance Computation with Polymorphic Methods. mypy#19466