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):
passHere, 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
- Invariant (default): Used when types are both read
from and written to (e.g., mutable collections like
list[T]). - Covariant (
covariant=True): Used when types are only produced or read (e.g., read-only collections likeSequence[T], function return types). - Contravariant (
contravariant=True): Used when types are only consumed or written (e.g., consumers, callable argument types).