Skip to content

Covariance and Contravariance

Rich Chiodo edited this page May 8, 2023 · 4 revisions

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.