47

Today I took a deep dive into Liskov's Substitution Principle and covariance/contravariance.

And I got stuck on the difference between:

  1. T = TypeVar("T", bound=Union[A, B])
  2. T = TypeVar("T", A, B, covariant=True)

My Understanding of #1

Difference between TypeVar('T', A, B) and TypeVar('T', bound=Union[A, B])

This answer clearly states T can be :

  1. Union[A, B] (or a union of any subtypes of A and B such as Union[A, BChild])
  2. A (or any subtype of A)
  3. B (or any subtype of B)

This makes perfect sense to me.


My Flawed Understanding of #2

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

Re-mentions the bound=Union[A, B] case, but does not get at the meaning of option #2, A, B, covariant=True.

I have tried playing around with mypy, and can't seem to figure it out. Can anyone point out what this means?

I think it means:

  1. A (or any subtype of A)
  2. B (or any subtype of B)

(aka it excludes the Union case from above)


**Edit**

It was asked in the comments:

Are you sure that they're actually different?

Here's sample code to show the difference. The errors come from mypy==0.770.

from typing import Union, TypeVar, Generic


class A: pass

class ASub(A): pass

class B: pass


# Case 1... Success: no issues found
# T = TypeVar("T", bound=Union[A, B])

# Case 2... error: Value of type variable "T" of "SomeGeneric" cannot be "ASub"
T = TypeVar("T", A, B, covariant=True)


class SomeGeneric(Generic[T]): pass

class SomeGenericASub(SomeGeneric[ASub]): pass

**Edit 2**

I ended up asking about this in python/mypy #8806: Generic[T_co] erroring when T_co = TypeVar("T_co", A, B, covariant=True) and passed subclass of A

This cleared up some misunderstandings I had. Turns out TypeVar("T", A, B, covariant=True) isn't really correct, knowing the value restrictions A and B aren't actually covariant.

Use of covariant=True syntax is only helpful when they're related.

Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
  • Are you sure that they're actually different? Unions are themselves covariant so it's not obvious to me that there's any difference between these two expressions. – Samwise May 03 '20 at 01:33
  • Yes @Samwise I just edited my question to add sample code demonstrating the difference – Intrastellar Explorer May 03 '20 at 02:02
  • I think mypy's handling of type variables with explicit options is currently buggy. With `T = TypeVar("T", A, B)`, even without covariance, it allows things like `x = SomeGeneric[ASub]()`, but not `x: SomeGeneric[ASub]`. It shouldn't allow the creation of values of type `SomeGeneric[ASub]` at all. – user2357112 May 03 '20 at 02:04
  • Okay, so maybe `mypy` is buggy. Can anyone at least explain to me what `T = TypeVar("T", A, B, covariant=True)` actually means? Are you saying that it should be identical to the `bound=Union[A, B]` case, and if yes, why? – Intrastellar Explorer May 03 '20 at 02:06
  • 1
    It *should* mean a type variable that can only be `A` or `B`, which happens to be covariant. It is not supposed to be identical to the union-bounded case. – user2357112 May 03 '20 at 02:08
  • For example, with `T = TypeVar("T", A, ASub, covariant=True)`, it would be okay to do `x: SomeGeneric[A] = SomeGeneric[ASub]()`, which would be invalid with `T = TypeVar("T", bound=Union[A, ASub])`. – user2357112 May 03 '20 at 02:10
  • @user2357112supportsMonica to me, `A, ASub, covariant=True` is sort of redundant to just saying `bound=A`. Would you agree? I think it makes more sense in the `A, B` case, which I sadly still don't understand :( – Intrastellar Explorer May 03 '20 at 02:19
  • @IntrastellarExplorer: `T = TypeVar("T", A, ASub, covariant=True)` would reject `SomeOtherASubclass`. – user2357112 May 03 '20 at 02:23
  • (Don't assume that any of this is useful just based on whether it's allowed. You can do all sorts of things that aren't very useful.) – user2357112 May 03 '20 at 02:24
  • @user2357112supportsMonica, I agree that `A, ASub, covariant=True` should reject `SomeOtherASub` because `SomeOtherASub` isn't a subclass of `ASub`. What about `T = TypeVar("T", A, B, covariant=True)`, and then testing with just plain ol' `ASub`? Do you think that it errors is a bug in `mypy`? I am still uncertain of the general recipe of `A, B, covariant=True` – Intrastellar Explorer May 03 '20 at 02:39

1 Answers1

75

Covariance and contra-variance are terms that relate to the intersection between object orientation and generics.

Here's the question this concept is trying to answer:

  1. We have a couple of "regular", "object-oriented" classes, Base and Derived.
  2. We also have some generic type - let's say List[T].
  3. We know that Derived can be used anywhere Base can - does that mean that List[Derived] can be used wherever List[Base] can?
  4. Could it be the other way around? Maybe it's the reverse direction and now List[Base] can be used wherever List[Derived] can?

If the answer to (3) is yes, it's called covariance and we'll say declare List as having covariance=True. If the answer to (4) is true, it's called contra-variance. If none is true, it's invariant.

Bounds also come from the intersection of OO and generics. When we define a generic type MyType<T> - does it mean that T can be any type at all? Or, may I impose some limitations on what T might be? Bounds allow me to state that the upper bound of T is, for example, the class Derived. In that case, Base can't be used with MyType - but Derived and all its subclasses can.

The definition of covariance and contravariance can be found in this section of PEP-484.

Piotr Czapla
  • 25,734
  • 24
  • 99
  • 122
Roy2012
  • 11,755
  • 2
  • 22
  • 35