Variance explains the most puzzling checker errors and is essential for designing generic APIs.
If int is acceptable where float is expected, is list[int] acceptable where list[float] is expected? No, and the reason is variance.
A function given list[float] may append 3.5. If the caller passed a list[int], its list now contains a float: type safety broken. Mutable containers are therefore invariant: list[int] and list[float] are unrelated.
Read-only containers are safe: Sequence[int] is a Sequence[float], since nothing can be inserted. They are covariant: the subtype relation goes the same direction.
Things that consume values are contravariant: a Callable[[float], None] can be used where Callable[[int], None] is expected, because a function that handles any float certainly handles ints. Parameters are contravariant, return types covariant.
For your own generics:
- With the 3.12 syntax (
class Box[T]), variance is inferred from howTis used: only in return positions means covariant, only in parameters contravariant, both (or a mutable attribute) invariant. - With old-style TypeVars you declare it:
TypeVar("T_co", covariant=True).
Practical fix for the common error: accept Sequence[float] or Iterable[float] instead of list[float] in parameters you don't mutate.
Example
from collections.abc import Callable, Sequence
def mean(xs: Sequence[float]) -> float: # covariant: list[int] is accepted
return sum(xs) / len(xs)
def add_half(xs: list[float]) -> None: # invariant: needs exactly list[float]
xs.append(0.5)
class Reader[T]: # T only returned -> inferred covariant
def __init__(self, value: T) -> None:
self._value = value
def get(self) -> T:
return self._value
def read_number(r: Reader[float]) -> float:
return r.get()
ints: list[int] = [1, 2, 3]
print(mean(ints), read_number(Reader(4))) # 2.0 4
handler: Callable[[int], object] = lambda x: float(x) # contravariant parameter
# add_half(ints) -> checker error: list[int] is not list[float] (list is invariant)Remember
- Mutable containers are invariant; read-only ones are covariant
- Parameters are contravariant, return types covariant
- Accept Sequence/Iterable instead of list to avoid invariance errors
Watch out for
- Typing a read-only parameter as list[float] and then fighting errors when callers pass list[int].