Python’s covariance and contravariance

Daniel Bastos
Magrathea
Published in
4 min readMay 14, 2018

--

(Many of the examples present are based on or from PEP 484)

Dynamic vs static typed languages

Static typing comic

Dynamic typed programming languages, such as Python, don’t worry about types; you don’t have to tell if a variable is of type “string”, “int”, “list” or anything else. Type checking, the process of verifying and enforcing the constraints of types, is done during run-time, as opposed to compile time. The pros is the short compilation time (after interpretation) and fast development pace. The cons, however, is that given type checking is done during run-time, silly errors that could be avoided in static typed languages may emerge.

On the other spectrum, static typed languages, like Java, run type checking at compile time (just as mentioned above). The benefits are focused on catching type mistakes before the program runs. If one is found, i.e. passing a string as an argument to a function that expects integer, it will be caught by the compiler and thus not compile.

Covariance and contravariance in static typed languages

In static typed languages, type checking obeys the type system, which has something called subtyping. For instance, if type Animal has Cat subtype, then type Cat may use an expression that type Animal uses. The word “may” is chosen because the ability to use or not an expression of a supertype or subtype on the current type is determined by something called “covariance” and “contravariance”.

Covariance is when subtypes, in our example, Cat, can use a less specific expression of its supertype, Animal. Contravariance is the opposite, it enables a supertype, Animal, to use an more specific expression of its subtype, Cat. In other words:

Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified — Microsoft

Apart from these two terms, there’s invariance, which does not let either of the above scenarios to occur. Cat cannot use Animal expressions and Animal, Cat expressions.

Covariance and contravariance are essential to static typed languages, however, not so for dynamic ones. In the latter one, one does not stop to think if they are able to call an Animal type expression on a Cat type.

Python type checking

With the focus on enhancing what was already present regarding optional static typing in Python, PEP 484 — Type Hints — was written. Pevious PEPs, like PEP 3107, had already introduced function annotations and other similar features.

Annotating a function can be as simple as the example below:

def greeting(name: str) -> str:
return 'Hello ' + name

Here, the argument name is of type str and the return type of the function is defined after the arrow, in this case, also str.

Generics can also be used. If the type information of a sequence (list) is unknown, it can at least be parametrized by the TypeVar factory:

from typing import Sequence, TypeVar

T = TypeVar('T') # Declare type variable

def first(l: Sequence[T]) -> T: # Generic function
return l[0]

TypeVar accepts a range of types. In the snippet above, none is written, therefore, any is accepted.

Parametrizing, for example, strings and integers in TypeVar would look like the following: T = TypeVar('T', str, int).

Note: type checking in Python is NOT done during run-time. Instead, it is expected that another service runs these checks, which users can execute before their program. MyPy is the most famous.

With the option of type checking now present in Python, covariance and contravariance come into notice.

Covariance and contravariance in Python type hints

From PEP 484's example:

Consider a class Employee with a subclass Manager. Now, suppose we have a function with an argument annotated withList[Employee]. Should we be allowed to call this function with a variable of type List[Manager] as its argument? Many people would answer “yes, of course” without even considering the consequences. But unless we know more about the function, a type checker should reject such a call: the function might append an Employee instance to the list, which would violate the variable’s type in the caller.

The intuitive answer here behaves covariantly, however, such argument is behaving contravariantly.

In Python, declaring if a container type is covariant or contravariant can be done with a simple extra passing of the arguments covariant=True or contravariant=True . By convention, variables ending with _co are covariant and _contra , contravariant.

Let’s use the following scenario: there’s a class Animal , one that inherits from it, Cat and a user-defined container type class that defines a read-only immutable list, ImmutableList. Since List_co is user-defined, it's characterized as a Generic Type.

from typing import TypeVar, Generic, Iterable, Iterator
List_co = TypeVar('List_co', covariant=True)
class ImmutableList(Generic[List_co]): def __init__(self, items: Iterable[List_co]) -> None:
...
...
def __iter__(self) -> Iterator[List_co]:
...
...

class Employee():
...
class Manager(Employee):
...

Since nothing is returned in __init__, its return is annotated with None. The Iterable type in the argument means that it has to be a type in which an iteration can be done upon, like a list. Iterator is the return type of __iter__ and it will iterate through the Iterable object.

Moving on, if we define a method that the type of its argument is ImmutableList[Employee] and it is called with type ImmutableList[Manager].

def list_employees(employees: ImmutableList[Employee]) -> None:
for employee in employees:
print(employee)
managers = ImmutableList[Manager()]
list_employees(managers)

it will work fine, because of the _co (covariant) notation (calling generic expressions of its supertype).

If covariant=true was not present, this would not be possible. Running the snipped above with MyPy (mypy <file-name>.py) in this scenario returns the following error:

… error: Argument 1 to “dump_employees” has incompatible type “ImmutableList[Manager]”; expected “ImmutableList[Employee]”

We can modify the snipped above and use contravariant=true to see what happens, but I’ll leave that up to you :)

--

--