-
Notifications
You must be signed in to change notification settings - Fork 973
Covariance and Contravariance
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):
passWhat does covariant, contravariant, and invariant mean in practice?
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 = catsBut this one is not:
cats = animalsWhich 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 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]):
passIs 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 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.