21
Python Type Hint: Contravariant, Covariant, Invariant
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]?
If A<: B, you can replace B with A anywhere
We say C[T] where T bound to B is
Contravariant, when
A <: B => C[A] :> C[B]
We can replace C[B] with C[A] anywhere.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.Invariant, If both circumstances do not suit. C[A] can not exchange with C[B], and vice versa.
Accordingly to their behaviors, We have two kinds of objects defined.
- Source[T]: It produces T, is covariant to T.
- 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.
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.
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.
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.
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.
21