[PEP 695] Fix incorrect Variance Computation with Polymorphic Methods. by randolf-scholz · Pull Request #19466 · python/mypy
That just feels horribly wrong. LSP asserts that, given two types T <: U, any valid operation on U must be valid on T as well. If an upcast removes type checking errors, then something went wrong.
But where is this violated? Given T@Mixed is treated as covariant, then test_new_sub passes. What doesn't pass without upcasting is the contravariant case test_new_super, where the key is a supertype of T, and that is fine, because upcasting here would mean we start treating the Mixed[str, int] instance as if it were Mixed[object, int].
Right now I'm moderately certain that Mixed should be contravariant in T (as inferred by current mypy master), not covariant as this PR and Pyright think.
Technically, in this example T should be both co- and contravariant simultaneously ("bi-variant"), since there is nothing inside the body of Mixed that would impose variance constraints on T. Usually in this case, both mypy and pyright would prefer covariance over contravariance by convention, for example both treat an empty generic class Foo[T]: pass as covariant (mypy-play, pyright-play.).
Whether Mixed ought to be co- or contravariant in T depends on how T is used within its body. But T is absent. polymorphic methods, like new in the example, that use a method-bound typevar do not impose constraints on the variance. (and this is the bug, mypy incorrectly infers a constraint here).
So the result is not technically wrong, as explained above T is technically both co- and contravariant, but (A) the way it is inferred is incorrect, and (B) it goes against the usual preference for covariance in the case of no constraints.
Moreover, if we added a covariant constraint like def get(self) -> T, then master would incorrectly infer Mixed as invariant, when it should be covariant. We can double-check this by using old-style typevars. If Mixed ought to be contravariant in T, then mypy should complain here, but it doesn't: (see also the original example of #19439)
from typing import TypeVar, Generic T = TypeVar("T", covariant=True, contravariant=False) U = TypeVar("U", covariant=False, contravariant=False) S = TypeVar("S", covariant=False, contravariant=False) class Mixed(Generic[T, U]): # OK, no variance error detected. def get(self) -> T: ... # force covariance def new(self: "Mixed[S, U]", key: S, val: U) -> None: ... # does not impose constraints on T
https://mypy-play.net/?mypy=latest&python=3.12&gist=811ae6429e7e0e05ba06e34d2daed000