From the official docs:
The introduction of explicit syntax for generic classes in Python 3.12 eliminates the need for variance to be specified for type parameters. Instead, type checkers will infer the variance of type parameters based on their usage within a class. Type parameters are inferred to be invariant, covariant, or contravariant depending on how they are used.
The problem is, type checkers include __init__
when infering variance: they check if __init__
accepts the typevar as init parameter.
class MyContainer[T]: # This is covariant for all intended usecases
# Inferred as invariant because of `val: T`, but I need to pass the value somehow...
def __init__(self, val: T) -> None:
self.val = val
def get_val(self) -> T:
return self.val
def print_float_val(container: MyContainer[float]) -> None:
print(container)
int_container = MyContainer(1) # reveal_type: MyContainer[int]
print_float_val(int_container)
# main.py:15: error: Argument 1 to "print_float_val" has incompatible type "MyContainer[int]"; expected "MyContainer[float]" [arg-type]
I cannot imagine this behavior to be very useful because:
tuple
, namedtuple
, frozenset
.__init__
when instantiating objects, and it is irrelevant afterward. For instance, print_float_val
accepts an instance of MyContainer[float]
, so the object would have been instantiated before being passed to this function.__init__
has nothing to do with it. T
is inferred to be invariant because val
(and thus its "setter") is considered public.
If you were to prefix the attribute name with an underscore (e.g., _val
), type checkers would know that it is private, and infer T
to be covariant.
(playgrounds: Mypy, Pyright, Pyrefly)
class MyContainer[T]:
def __init__(self, val: T) -> None:
self._val = val
def get_val(self) -> T: ...
def takes_int_container(container: MyContainer[int]) -> None: ...
takes_int_container(MyContainer[object](object())) # error
takes_int_container(MyContainer[int](int())) # fine
takes_int_container(MyContainer[bool](bool())) # fine
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With