Search code examples
python-3.xgenericscovariancemypy

MyPy doesn't allow constrained TypeVar's to be covariant? Defining a generic dict with constrained but covariant key-val types


I'm trying to define a custom generic dict, whose keys are of type T_key and values are of type T_val.
I also want to put constraints on T_key and T_val, such that T_key can only be of type A or B or their subclass.

How do I accomplish this?

from typing import TypeVar, Generic

class A: ...
class B: ...

class Asub(A): ...
class Bsub(B): ...

T_key = TypeVar('T_key', A, B, covariant=True)
T_val = TypeVar('T_val', A, B, covariant=True)


class MyDict(Generic[T_key, T_val]): ...


w: MyDict[   A,    B]
x: MyDict[   A, Bsub]
y: MyDict[Asub,    B]
z: MyDict[Asub, Bsub]

When I try to check this, mypy gives errors on annotations of x, y and z. Only the annotation for w works as expected.

generic.py:17: error: Value of type variable "T_val" of "MyDict" cannot be "Bsub"
generic.py:18: error: Value of type variable "T_key" of "MyDict" cannot be "Asub"
generic.py:19: error: Value of type variable "T_key" of "MyDict" cannot be "Asub"
generic.py:19: error: Value of type variable "T_val" of "MyDict" cannot be "Bsub"

I don't understand why Asub is not a valid type for T_key even with covariant=True specified.

What am I missing here?

mypy version: 0.630


Solution

  • That's not what covariance means. With a covariant type variable T and a generic class Foo[T], instances of Foo[Subclass] are also considered instances of Foo[Superclass]. Covariance has no effect on what types may be substituted for T.

    If your B were defined as

    class B(A): ...
    

    instead of

    class B: ...
    

    , then a value of type MyDict[B, B] would be considered a valid value of type MyDict[A, A] by static type checkers due to covariance. You would still not be able to create a value of type MyDict[ASub, BSub], because the only valid values of your type variables are A and B.

    The concept you're looking for is bounded type variables, using the bound keyword argument, not constrained type variables. It looks like you can specify a union as the bound, which comes as quite a surprise to me, so declaring the type variables as

    T_key = TypeVar('T_key', bound=Union[A, B])
    T_val = TypeVar('T_val', bound=Union[A, B])
    

    should work.