3.12. Typing Variance

  • https://www.youtube.com/watch?v=1IiL31tUEVk&t=445s

  • Covariance - Enables you to use a more derived type than originally specified

  • Contravariance - Enables you to use a more generic (less derived) type than originally specified

  • Invariance - Means that you can use only the type originally specified.

  • Invariance is important for example in np.ndarray, where all items must be exactly the same type

Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types [1]

In general, a covariant type parameter can be used as the return type of a delegate, and contravariant type parameters can be used as parameter types.

3.12.1. SetUp

>>> from typing import TypeVar

3.12.2. Recap

>>> bool.mro()
[<class 'bool'>, <class 'int'>, <class 'object'>]
>>> isinstance(True, bool)
True
>>> isinstance(True, int)
True
>>> isinstance(True, object)
True
>>> isinstance(1, bool)
False
>>> isinstance(1, int)
True
>>> isinstance(1, object)
True
>>> isinstance(object, bool)
False
>>> isinstance(object, int)
False
>>> isinstance(object, object)
True

3.12.3. Invariance

  • Must be the same type

Means that you can use only the type originally specified. An invariant generic type parameter is neither covariant nor contravariant [1]

>>> T = TypeVar('T', bound=int, covariant=False, contravariant=False)
>>>
>>> def run(x: T):
...     pass
>>> run(object)   # error
>>> run(1)        # ok
>>> run(True)     # error

In the example above, we specified x: int, so invariant will be run(1), others will yield an error.

3.12.4. Covariance

  • Must be the same type or it's subclass

Enables you to use a more derived type than originally specified [1]

>>> T = TypeVar('T', bound=int, covariant=True)
>>>
>>> def run(x: T):
...     pass
>>> run(object)   # error
>>> run(1)        # ok
>>> run(True)     # ok

3.12.5. Contravariance

  • Must be the same type or it's superclass

Enables you to use a more generic (less derived) type than originally specified [1]

>>> T = TypeVar('T', bound=int, contravariant=True)
>>>
>>> def run(x: T):
...     pass
>>> run(object)   # ok
>>> run(1)        # ok
>>> run(True)     # error

3.12.6. Use Case - 1

>>> Number = TypeVar('Number', int, float, covariant=True)
>>>
>>> def add(a: Number, b: Number) -> Number:
...     return a + b
>>> class PositiveInteger(int):
...     pass
>>> a = PositiveInteger(1)
>>> b = PositiveInteger(2)
>>>
>>> add(a, b)  # ok (covariant - int|float or their subclasses, like: PositiveInteger)
3

3.12.7. Use Case - 2

>>> Number = TypeVar('Number', int, float, covariant=False, contravariant=False)  # invariant
>>>
>>> def add(a: Number, b: Number) -> Number:
...     return a + b
>>> class PositiveInteger(int):
...     pass
>>> a = PositiveInteger(1)
>>> b = PositiveInteger(2)
>>>
>>> add(a, b)  # error (invariant - must be int|float)
3

3.12.8. References