Skip to content

Commit ecfab6a

Browse files
authored
[PEP 695] Allow Self return types with contravariance (#17786)
Fix variance inference in this fragment from a typing conformance test: ``` class ClassA[T1, T2, T3](list[T1]): def method1(self, a: T2) -> None: ... def method2(self) -> T3: ... ``` Previously T2 was incorrectly inferred as invariant due to `list` having methods that return `Self`. Be more flexible with return types to allow inferring contravariance for type variables even if there are `Self` return types, in particular. We could probably make this even more lenient, but after thinking about this for a while, I wasn't sure what the most general rule would be, so I decided to just make a tweak to support the likely most common use case (which is probably actually not that common either). Link to conformance test: https://github.com/python/typing/blob/main/conformance/tests/generics_variance_inference.py#L15C1-L20C12
1 parent 9518b6a commit ecfab6a

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

mypy/subtypes.py

+27
Original file line numberDiff line numberDiff line change
@@ -2024,6 +2024,16 @@ def infer_variance(info: TypeInfo, i: int) -> bool:
20242024

20252025
typ = find_member(member, self_type, self_type)
20262026
if typ:
2027+
# It's okay for a method in a generic class with a contravariant type
2028+
# variable to return a generic instance of the class, if it doesn't involve
2029+
# variance (i.e. values of type variables are propagated). Our normal rules
2030+
# would disallow this. Replace such return types with 'Any' to allow this.
2031+
#
2032+
# This could probably be more lenient (e.g. allow self type be nested, don't
2033+
# require all type arguments to be identical to self_type), but this will
2034+
# hopefully cover the vast majority of such cases, including Self.
2035+
typ = erase_return_self_types(typ, self_type)
2036+
20272037
typ2 = expand_type(typ, {tvar.id: object_type})
20282038
if not is_subtype(typ, typ2):
20292039
co = False
@@ -2066,3 +2076,20 @@ def infer_class_variances(info: TypeInfo) -> bool:
20662076
if not infer_variance(info, i):
20672077
success = False
20682078
return success
2079+
2080+
2081+
def erase_return_self_types(typ: Type, self_type: Instance) -> Type:
2082+
"""If a typ is function-like and returns self_type, replace return type with Any."""
2083+
proper_type = get_proper_type(typ)
2084+
if isinstance(proper_type, CallableType):
2085+
ret = get_proper_type(proper_type.ret_type)
2086+
if isinstance(ret, Instance) and ret == self_type:
2087+
return proper_type.copy_modified(ret_type=AnyType(TypeOfAny.implementation_artifact))
2088+
elif isinstance(proper_type, Overloaded):
2089+
return Overloaded(
2090+
[
2091+
cast(CallableType, erase_return_self_types(it, self_type))
2092+
for it in proper_type.items
2093+
]
2094+
)
2095+
return typ

test-data/unit/check-python312.test

+57-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ b: Invariant[int]
213213
if int():
214214
a = b # E: Incompatible types in assignment (expression has type "Invariant[int]", variable has type "Invariant[object]")
215215
if int():
216-
b = a # E: Incompatible types in assignment (expression has type "Invariant[object]", variable has type "Invariant[int]")
216+
b = a
217217

218218
c: Covariant[object]
219219
d: Covariant[int]
@@ -393,6 +393,62 @@ inv3_1: Invariant3[float] = Invariant3[int](1) # E: Incompatible types in assig
393393
inv3_2: Invariant3[int] = Invariant3[float](1) # E: Incompatible types in assignment (expression has type "Invariant3[float]", variable has type "Invariant3[int]")
394394
[builtins fixtures/property.pyi]
395395

396+
[case testPEP695InferVarianceWithInheritedSelf]
397+
from typing import overload, Self, TypeVar, Generic
398+
399+
T = TypeVar("T")
400+
S = TypeVar("S")
401+
402+
class C(Generic[T]):
403+
def f(self, x: T) -> Self: ...
404+
def g(self) -> T: ...
405+
406+
class D[T1, T2](C[T1]):
407+
def m(self, x: T2) -> None: ...
408+
409+
a1: D[int, int] = D[int, object]()
410+
a2: D[int, object] = D[int, int]() # E: Incompatible types in assignment (expression has type "D[int, int]", variable has type "D[int, object]")
411+
a3: D[int, int] = D[object, object]() # E: Incompatible types in assignment (expression has type "D[object, object]", variable has type "D[int, int]")
412+
a4: D[object, int] = D[int, object]() # E: Incompatible types in assignment (expression has type "D[int, object]", variable has type "D[object, int]")
413+
414+
[case testPEP695InferVarianceWithReturnSelf]
415+
from typing import Self, overload
416+
417+
class Cov[T]:
418+
def f(self) -> Self: ...
419+
420+
a1: Cov[int] = Cov[float]() # E: Incompatible types in assignment (expression has type "Cov[float]", variable has type "Cov[int]")
421+
a2: Cov[float] = Cov[int]()
422+
423+
class Contra[T]:
424+
def f(self) -> Self: ...
425+
def g(self, x: T) -> None: ...
426+
427+
b1: Contra[int] = Contra[float]()
428+
b2: Contra[float] = Contra[int]() # E: Incompatible types in assignment (expression has type "Contra[int]", variable has type "Contra[float]")
429+
430+
class Cov2[T]:
431+
@overload
432+
def f(self, x): ...
433+
@overload
434+
def f(self) -> Self: ...
435+
def f(self, x=None): ...
436+
437+
c1: Cov2[int] = Cov2[float]() # E: Incompatible types in assignment (expression has type "Cov2[float]", variable has type "Cov2[int]")
438+
c2: Cov2[float] = Cov2[int]()
439+
440+
class Contra2[T]:
441+
@overload
442+
def f(self, x): ...
443+
@overload
444+
def f(self) -> Self: ...
445+
def f(self, x=None): ...
446+
447+
def g(self, x: T) -> None: ...
448+
449+
d1: Contra2[int] = Contra2[float]()
450+
d2: Contra2[float] = Contra2[int]() # E: Incompatible types in assignment (expression has type "Contra2[int]", variable has type "Contra2[float]")
451+
396452
[case testPEP695InheritInvariant]
397453
class Invariant[T]:
398454
x: T

test-data/unit/pythoneval.test

+20
Original file line numberDiff line numberDiff line change
@@ -2196,3 +2196,23 @@ type K4 = None | B[int]
21962196

21972197
type L1 = Never
21982198
type L2 = list[Never]
2199+
2200+
[case testPEP695VarianceInferenceSpecialCaseWithTypeshed]
2201+
# flags: --python-version=3.12
2202+
class C1[T1, T2](list[T1]):
2203+
def m(self, a: T2) -> None: ...
2204+
2205+
def func1(p: C1[int, object]):
2206+
x: C1[int, int] = p
2207+
2208+
class C2[T1, T2, T3](dict[T2, T3]):
2209+
def m(self, a: T1) -> None: ...
2210+
2211+
def func2(p: C2[object, int, int]):
2212+
x: C2[int, int, int] = p
2213+
2214+
class C3[T1, T2](tuple[T1, ...]):
2215+
def m(self, a: T2) -> None: ...
2216+
2217+
def func3(p: C3[int, object]):
2218+
x: C3[int, int] = p

0 commit comments

Comments
 (0)