We briefly encountered the concept of covariance in the previous article. Now lets take a deeper look.

Variance refers to places where one data type can be substituted for another.

• Covariance means that a data type can be substituted with a more specific type
• Contravariance means that a data type can be substituted with a more general type
• Invariance means that a data type cannot be substituted either way

The most common case is inheritance. Here is the example from the previous article

``````class Person:
def __init__(self, name: str) -> None:
self.name = name

def greet(self) -> str:
return f'Hello {self.name}'

class Employee(Person):
def __init__(self, name: str, employee_id: int) -> None:
super().__init__(name)
self.employee_id = employee_id

print(f'logging in with it {self.employee_id}')``````

If we declare a function that expects a type `Person`, we can safely pass in an object of type `Employee`

``````def hello(p: Person):
print(p.greet())

aparna: Employee = Employee('Aparna', 3)
hello(aparna) # ✅ OK``````

The formal computer science term for this behaviour is covariance.

💡
A data type is covariant if it can be safely substituted with a more specific type.

In Python, normal classes are covariant, meaning we can safely pass in a child class when the parent class is expected.

## Container Types

Most people find parent-child covariance quite intuitive. But things get complex when generic types are involved. We already know that `Employee` is covariant with `Person`. What about `list[Employee]` and `list[Person]`? Are they covariant?

Consider the code below, is it correct?

``````def add_person(p: list[Person]) -> None:
p.append(Person("Anjali"))

people: list[Employee] = [Employee('Aparna', 3)]
for p in people:

Seeing this example, it should be clear that the code is not correct. `people` is a list of `Employee`. When we pass the list to `add_person`, the function is adding a `Person` object into this list which is incorrect. When we return from the function, in the loop we do `p.login()` and this will fail since the `Person` object that was added does not have that method.

Running `mypy` on the code above gives this error

``test.py:23: error: Argument 1 to "add_person" has incompatible type "List[Employee]"; expected "List[Person]"  [arg-type]``

From this we can conclude that `list[Employee]` is not covariant with `list[People]`. We cannot substitute `list[Employee]` in a place where `list[People]` is expected. A similar examination will show that the opposite is also not possible. We cannot substitute `list[People]` in a place where `list[Employee]` is expected.

This means that the generic type `list[T]` is invariant.

💡
A data type is invariant if it cannot be substituted with a more specific type, and it also cannot be substituted with a more general type

On the other hand, consider a function that only reads values from the container, without adding anything to it. In the example below, we can use the `Sequence` type instead of `list`. A `Sequence` only allows iteration of the data.

``````from typing import Sequence

def print_all(people: Sequence[Person]) -> None:
for p in people:
print(p.greet())

people: list[Employee] = [Employee('Aparna', 3)]
print_all(people) # ✅ OK``````

Here, it is safe to pass an object of type `list[Employee]` to the function `print_all` (A `list` is also a `Sequence` because is can be iterated).

So the data type `Sequence[T]` is covariant.

Generally speaking, container types that only read values from the container (eg: `Sequence`) are covariant. While those that contain methods for both reading and adding new items (eg: `list`) are invariant.

## Function Types

In Python, functions are first class objects. We can pass functions as parameters or return functions from other functions. There is a need to make a function signature as a data type. In Python. it is represented by the `Callable` generic type. Here is an example to make it clearer

``````from typing import Callable

def process_people(
employees: list[Employee],
fn: Callable[[Employee], str]
) -> list[str]:
return [fn(p) for p in employees]``````

Look at the data type of the `fn` parameter: `Callable[[Employee], str]`

For this parameter we should pass a function that takes a single input parameter of type `Employee` and returns an output of type `str`.

``````def get_employee_display(p: Employee):
return f"{p.name} [{p.employee_id}]"

people: list[Employee] = [Employee("Anjali", 1), Employee("Aparna", 2)]
displays = process_people(people, get_employee_display) # ✅ OK
print(displays) # ['Anjali [1]', 'Aparna [2]']``````

Now consider this function that takes `Person` as input instead of `Employee`

``````def get_person_display(p: Person):
return p.greet()``````

Can we pass this function as a parameter to `process_people`? Lets try

``````people: list[Employee] = [Employee("Anjali", 1), Employee("Aparna", 2)]
displays = process_people(people, get_person_display) # ✅ OK
print(displays) # ['Hello Anjali', 'Hello Aparna']``````

It works! `mypy` does not give any error, and the code runs.

We can see why it works: `process_people` will be passing `Employee` objects as input to the `fn` parameter. Any function that can process `Employee` can be used here – that means any function that takes input as `Employee` or any of its parent types.

This shows that the `Callable` type is contravariant with respect to the input types.

💡
A data type is contravariant if it can be substituted with a more general type

On the other hand, `Callable` is covariant with respect to the return types. Which means we can use `Callable[[str], Employee]` in a place where `Callable[[str], Person]` is expected.

## Summary

To summarise, the concept of variance explains when one type can be substituted with another type

• Type variables that are covariant can be substituted with a more specific type without causing errors
• Type variables that are contravariant can be substituted with a more general type without causing errors
• Types where neither is possible are invariant

Among the common use cases in Python, the following is the behaviour

• Normal classes and types are covariant
• Mutable container types are invariant
• Read-only container types are covariant
• Function types are contravariant with respect to the input types
• Function types are covariant with respect to the output type

Many programmers intuitively understand that you can use a `Child` type where a `Parent` type is expected. So they get confused when they try to substitute `list[Child]` in a place where `list[Parent]` is required and get errors. Hopefully this article sheds some light on the concept.