Invariant, Covariant, and Contravariant in Python

In Python's static type system, variance describes how subtyping relationships between base types relate to the subtyping relationships between generic types that use them. Understanding the difference between invariance, covariance, and contravariance allows developers to properly annotate generic classes, functions, and collections. By defining whether a generic container or callable accepts subtypes, supertypes, or only exact type matches, variance rules prevent subtle runtime type errors while keeping code flexible.

The Subtyping Baseline

Consider a simple inheritance hierarchy:

class Animal:
    pass

class Dog(Animal):
    pass

Here, Dog is a subtype of Animal (Dog \(\le\) Animal). Variance answers the question: If Dog is a subtype of Animal, what is the relationship between Container[Dog] and Container[Animal]?


1. Invariance

A type variable is invariant when generic types preserve no subtyping relationship, regardless of the relationship between their underlying type arguments. If Dog is a subtype of Animal, Container[Dog] has no relation to Container[Animal]. Neither can be substituted for the other.

In Python, type variables created with TypeVar are invariant by default:

from typing import TypeVar, Generic

T = TypeVar('T')  # Invariant by default

class Box(Generic[T]):
    def __init__(self, content: T) -> None:
        self.content = content

def inspect_animal_box(box: Box[Animal]) -> None:
    pass

dog_box: Box[Dog] = Box(Dog())
inspect_animal_box(dog_box)  # Type-checker error: Box[Dog] is not Box[Animal]

Why it is used: Invariance is necessary for mutable containers (like list or dict). If list[Dog] were accepted where a list[Animal] is expected, a function could append a Cat() into the list, violating the type safety of the original list[Dog].


2. Covariance

A type variable is covariant when generic types preserve the original subtyping relationship in the same direction. If Dog is a subtype of Animal, then Container[Dog] is a subtype of Container[Animal].

To declare covariance in Python, set covariant=True in TypeVar:

from typing import TypeVar, Generic

T_co = TypeVar('T_co', covariant=True)

class ReadOnlyBox(Generic[T_co]):
    def __init__(self, content: T_co) -> None:
        self._content = content

    def get(self) -> T_co:
        return self._content

def print_animal_name(box: ReadOnlyBox[Animal]) -> None:
    animal = box.get()
    print(animal)

dog_box: ReadOnlyBox[Dog] = ReadOnlyBox(Dog())
print_animal_name(dog_box)  # Valid: ReadOnlyBox[Dog] is a subtype of ReadOnlyBox[Animal]

Why it is used: Covariance is safe for read-only containers or "producers." If a container only outputs data (like tuple, Sequence, or return types of functions), retrieving a Dog satisfies any code expecting an Animal.


3. Contravariance

A type variable is contravariant when generic types reverse the original subtyping relationship. If Dog is a subtype of Animal, then Container[Animal] becomes a subtype of Container[Dog].

To declare contravariance in Python, set contravariant=True in TypeVar:

from typing import TypeVar, Generic

T_contra = TypeVar('T_contra', contravariant=True)

class Sink(Generic[T_contra]):
    def send(self, value: T_contra) -> None:
        pass

def handle_dog(sink: Sink[Dog]) -> None:
    sink.send(Dog())

animal_sink: Sink[Animal] = Sink()
handle_dog(animal_sink)  # Valid: Sink[Animal] is a subtype of Sink[Dog]

Why it is used: Contravariance is safe for write-only destinations or "consumers." A handler capable of processing any generic Animal can safely handle a Dog, making Sink[Animal] valid wherever Sink[Dog] is needed. This is the foundation of function parameter typing: a function accepting broader types can safely replace a function expecting narrower types.


Summary Rule of Thumb