What does covariant and contravariant mean in a Python scenario?
The official description of using 'covariant' or 'contravariant' is here: https://peps.python.org/pep-0483/#covariance-and-contravariance
Given two classes:
class Animal():
pass
class Cat(Animal):
pass
What does covariant, contravariant, and invariant mean in practice?
Covariance
Covariance means that something that accepts an Animal, can also accept a Cat. Covariance goes up
the hierarchy tree. In python common covariant objects are non mutable collections, like say a 'tuple'.
cat1 = Cat()
dog1 = Dog()
animals: Tuple[Animal] = (animal1, dog1)
cats: Tuple[Cat] = (cat1, cat1)
Since Tuple is covariant, this assignment is allowed:
animals = cats
But this one is not:
cats = animals
Which if you think about it, makes sense. If you were passing a tuple of animals to someone, the fact that the tuple had dogs in it wouldn't mess anything up. You'd only call methods on the base class Animal
in that scenario. But if you were passing a tuple of cats to someone, they wouldn't expect anything in that tuple to bark
.
Contravariance
Contravariance is harder to understand. It goes 'down' the hierarchy tree. In python this happens for Callable
.
Suppose we had code like this:
T = TypeVar('T')
def eat(a: Animal):
pass
def bark(d: Dog):
pass
def do_action(animal: T, action: Callable[[T], None]):
pass
Is this allowed?
do_action(dog1, bark)
Intuitively it makes sense that would work, but what about this?
do_action(dog1, eat)
That is allowed because the callable is contravariant in its arguments. Meaning if it takes an animal, you can pass it any subclass.
This however would not be allowed:
do_action(cat1, bark)
Because the bark function is a Callable[[Dog], None]
and Cat
is not contravariant with Dog
. Meaning you can't go down from Cat
to Dog
.
This would work though.
class Shepherd(Dog):
pass:
dog2 = Shepherd()
do_action(dog2, bark)
Invariance
Invariance means you can only pass the exact same type for an object. Meaning if something takes a Cat
and that something is invariant, you can't pass it an Animal
.
Here's an example:
cats: list[Cat] = [cat1, cat2, cat3]
This code would be an error, which seems obvious:
cats.append(dog1)
but also this code is an error which is less obvious:
iguana1 = Animal()
cats.append(iguana1)
List is invariant
. Meaning when you get a list of cats, it is guaranteed to only have cats in it.