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
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.
Combine the concept with Sink / Source.
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.
Top comments (2)
Hi,
Thanks for taking the time to explain. Your explanation seemed clear to be, however mypy complains on
sink_derived.consume(base)
with messageerror: Argument 1 to "consume" of "Sink" has incompatible type "Base"; expected "Derived" [arg-type]
Bug in mypy?I think I understand why mypy complains and it is correct to do so.
Co/contravariance define how much freedom we have to substitute a generic class with another generic class. Contravariance allows to assign
SinkBase()
to aSink[Derived]
variable.However, co/contravariance do not apply to method definitions. Contravariance does NOT say that we are allowed feed a Base object to a method that expects a Derived object!
sink_derived
is typed asSink[Derived]
. Sosink_derived.consume()
has signatureconsume(self, value: Derived)
. We must still honor this signature and call consume() with a Derived or a subclass of Derived.