2

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

Pragy Agarwal
  • 578
  • 1
  • 8
  • 22

2 Answers2

5

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • I'm confused. If I still can't make `MyDict[C, C]`, then what allows me to make `MyDict[B, B]`? Do you mean that with covariance I can't create `MyDict[B, B]` either, but if I could then it would be considered a subclass of `MyDict[A, A]`? Anyway, is there a way I could accomplish the desired behavior? Thanks :) – Pragy Agarwal Oct 25 '18 at 20:51
  • 1
    @PragyAgarwal: You can make `MyDict[B, B]` because `B` is one of the options you specified when you created `T_key` and `T_val`. As for doing what you're looking for, the concept you're looking for is bounded type variables, but I don't think Python supports the kind of bound you want. – user2357112 Oct 25 '18 at 20:55
  • That makes sense. So if `bound=` allowed me to provide more than one class (which it currently doesn't), I could have accomplished this? Is there any other workaround that you could suggest? Thanks :) – Pragy Agarwal Oct 25 '18 at 20:58
  • 1
    @PragyAgarwal: Maybe see if it makes sense to introduce a common superclass for `A` and `B`? Or see if specifying a union as the bound does anything useful? (I would be surprised if the union works; I don't think mypy's concept of subclassing works like that.) – user2357112 Oct 25 '18 at 21:03
  • 1
    Can't extract a superclass unfortunately, since it will be object. Yeah, Union works :D Just discovered it via https://github.com/python/mypy/issues/2702 I had not tried it since TypeVar needs concrete types for the constraint parameters. – Pragy Agarwal Oct 25 '18 at 21:06
  • 1
    @PragyAgarwal: Huh, it does work. My experiments also show a union-bounded type variable accepting types listed in the union or any of their subclasses, and rejecting non-subclasses. – user2357112 Oct 25 '18 at 21:17
  • @user2357112 Can you clarify in your second paragraph, including what types you are talking about? I am assuming it is ``TypeVar('T_key', A, B, covariant=True)`` and ``A`` and ``B(A)``, but not sure. – MisterMiyagi Feb 28 '19 at 13:15
1

solution:

Turns out bound can accept Unions.

from typing import TypeVar, Generic, Union

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

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



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


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


w: MyDict[   A,    B]  # passes
x: MyDict[   A, Bsub]  # passes
y: MyDict[Asub,    B]  # passes
z: MyDict[Asub, Bsub]  # passes
bad: MyDict[int, int]  # Type argument "builtins.int" of "MyDict" must be a subtype of "Union[generic.A, generic.B]"
Pragy Agarwal
  • 578
  • 1
  • 8
  • 22