Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does variance inference for type parameters include `__init__`?

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:

  • My understanding of covariant generics is that they are mostly (perhaps all) read-only containers. Pretty much all my covariant container types that I can think of receive the values it contains during initialization: tuple, namedtuple, frozenset.
  • We only call __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.
like image 395
Leonardus Chen Avatar asked Sep 15 '25 12:09

Leonardus Chen


1 Answers

__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
like image 118
InSync Avatar answered Sep 17 '25 01:09

InSync