Python Type Hint: Contravariant, Covariant, Invariant
Tags: python, generic, typehint
Published: 2021-12-07 17:33:35

We all know the Liskov substitution principle. The type can be replaced by its subtype without breaking it. But how about the relationship of their generic types, C[subtype] and C[type]?

## So, what is covariant?

If A<: B, you can replace B with A anywhere
We say C[T] where T bound to B is
1. Contravariant, when `A <: B => C[A] :> C[B]` 
We can replace C[B] with C[A] anywhere.

2. Covariant, when `A <: B => C[A] <: C[B]`
We can replace C[A] with C[B] anywhere. But How to cause this situation? I will explain in the example code below.

3. Invariant, If both circumstances do not suit. C[A] can not exchange with C[B], and vice versa.

## Combine the concept with Sink / Source. 

Accordingly to their behaviors, We have two kinds of objects defined.

1. Source[T]: It produces T,  is covariant to T. 
2. Sink[T]: It consumes T, is contravariant to T.

It is pretty abstract, so we directly move forward to the code below. Try to experiment by modifying according to comments.

```python
import abc

from typing import Generic, TypeVar

class Base:
    def foo(self):
        print("foo")

class Derived(Base):
    def bar(self):
        print("bar")
```
First, we have `Base` and `Derived.` `Derived` is inherent from `Base,` so we have `Derived <: Base.`

```python
T_co = TypeVar('T_co', bound='Base', covariant=True)

class Source(Generic[T_co]):
    @abc.abstractmethod
    def generate(self) -> T_co: # Produce T_co!
        pass

class SourceBase(Source[Base]):
    def generate(self) -> Derived: # Produce T_co!
        return Derived() 

class SourceDerived(Source[Derived]):
    def generate(self) -> Derived:
        return Derived() 

source: Source[Base] = SourceDerived()
source.generate()

#Try to uncomment lines below.
#source_derived: Source[Derived] = SourceBase()
#source_derived.generate()
```
Now, we have `SourceDerived <: SourceBase`. If we remove `covariant=True`, we will get this warning:
> [Pyright reportGeneralTypeIssues] [E] Expression of type > "SourceDerived" cannot be assigned to declared type "Source[Base]"
>  TypeVar "T_co@Source" is invariant
>    "Derived" is incompatible with "Base"

If you modify `covariant` to `contravariant,` this is what happened. `covariant` tells the checker SourceDerived can be safely used anywhere we use SourceBase.

``` python
    def generate(self) -> T_co: <- warining
        pass
```
> warining: [Pyright reportGeneralTypeIssues] [E] Contravariant type variable cannot be used in return type
>  TypeVar "T_co@Source" is contravariant

Look like `covariant` not only to check the place you use C[T_co] but also to check the method in C[T_co] return T_co.

Next, we take a look at the `contravariant` example.

```python
T_contra = TypeVar('T_contra', bound='Base', contravariant=True)

class Sink(Generic[T_contra]):
    @abc.abstractmethod
    def consume(self, value: T_contra):
        pass

class SinkBase(Sink[Base]):
    def consume(self, value: Base):
        value.foo()

class SinkDerived(Sink[Derived]):
    def consume(self, value: Derived):
        value.bar()

    def other_func(self):
        pass

base = Base()
derived = Derived()
sink_derived: Sink[Derived] = SinkBase()
#we can safely consumer
sink_derived.consume(base)
sink_derived.consume(derived)
#Try to uncomment this line.
#sink_derived.other_func()
```
We have `SinkDerive <: SinkBase` here. Removing `contravariant=True` will get warning:
> [Pyright reportGeneralTypeIssues] [E] Expression of type "SinkBase" cannot be assigned to declared type > "Sink[Derived]"
>    TypeVar "T_contra@Sink" is invariant
>    "Base" is incompatible with "Derived"

`contravariant=True` tells the static checker that the Base can be safely consumed Base or Derive type. Although we have annotated T_contra is contravariant, we will get an error if we call a method of Sink[Derived], `sink_derived.other_func()` for example, of course. Nevertheless, it is pretty common to assume that `contravariant` is opposite to `covariant.`


I think in most situations, we don't need to add `contravariant` or `covariant` right away. Only when the checker complains, do we look closely at the relationship of these generic types if used properly. If it is, we consider adding these hints, then.