TIL: Covariant vs Contravariant Types

When working with type systems in programming languages like TypeScript, Scala, Java, or even Python (via type hints), you'll encounter the concepts of covariance and contravariance. These terms describe how the subtype relationships of generic types behave when you substitute their type parameters. Understanding them can help you design safer, more expressive APIs.
What is Variance?
Imagine a generic type Box[T]
and types Cat
and Animal
where Cat
is a subtype of Animal
(Cat <: Animal
). Variance answers:
What is the relationship betweenBox[Cat]
andBox[Animal]
?
There are three possibilities:
Relationship | Meaning | Name |
---|---|---|
Box[Cat] <: Box[Animal] |
Subtyping preserved | Covariant |
Box[Animal] <: Box[Cat] |
Subtyping reversed | Contravariant |
No relationship | Subtyping not allowed in either direction | Invariant |
Covariant Types
A generic type is covariant in T
if it maintains the same direction of subtyping:
IfCat <: Animal
, thenBox[Cat] <: Box[Animal]
Use this when your type only produces values of T
. Think of read-only containers:
class ImmutableBox<+T> {
get(): T
}
Here, it's safe to use an ImmutableBox<Cat>
where an ImmutableBox<Animal>
is expected because values are only being read.
Contravariant Types
A type is contravariant in T
if it reverses the direction of subtyping:
IfCat <: Animal
, thenPrinter[Animal] <: Printer[Cat]
Use this when your type only consumes values of T
. For example, function inputs:
class Printer[-T] {
print(value: T): void
}
A Printer[Animal]
can be used wherever a Printer[Cat]
is needed—it can handle anything a Printer[Cat]
would.
Invariant Types
Most types are invariant by default:
class Box(Generic[T]):
def get(self) -> T: ...
def set(self, value: T): ...
Since Box[T]
both reads and writes T
, neither covariance nor contravariance is safe.
When to Use What?
Variance | Use Case |
---|---|
Covariant | Your type produces T (read-only) |
Contravariant | Your type consumes T (inputs, callbacks) |
Invariant | Your type both produces and consumes T |
Python Type Hints Example
Python supports variance via typing.TypeVar
:
from typing import TypeVar, Generic
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
class Producer(Generic[T_co]):
def get(self) -> T_co: ...
class Consumer(Generic[T_contra]):
def consume(self, item: T_contra) -> None: ...
This allows you to encode variance into your Python type hints, even if not enforced at runtime.
Understanding variance helps clarify type relationships in your code, especially when working with generics, callbacks, and collections. Reach for covariance when you're returning data, contravariance when you're taking it in, and invariance when you're doing both.