Variance in Python Generics: Covariance, Contravariance & Invariance

Variance is the rule that decides whether list[Dog] is an acceptable value where list[Animal] is expected. It is the single concept behind most confusing generic errors a static analyzer reports, and it is what separates a sound type from one that quietly lets a bug through. This guide covers the three variances — covariant, contravariant, and invariant — how to declare them on a TypeVar, and exactly how mypy and pyright enforce each one. For the underlying generic mechanics, start with the parent overview of Advanced Typing Patterns & Generics.

The three variances Covariant types preserve the subtype direction, contravariant types reverse it, and invariant types accept neither. Given Dog is a subtype of Animal… Covariant Sequence[Dog] ↓ is a Sequence[Animal] read-only producers Invariant list[Dog] ✗ neither way list[Animal] mutable containers Contravariant Callback[Animal] ↓ is a Callback[Dog] consumers / sinks
Covariance preserves the subtype direction, contravariance reverses it, invariance forbids both.

Syntax spec: declaring variance on a TypeVar

Before PEP 695, variance was declared explicitly with the covariant and contravariant keyword arguments to TypeVar. A plain TypeVar is invariant by default.

# Python 3.8+, legacy explicit-variance syntax
from typing import TypeVar, Generic

T_co = TypeVar("T_co", covariant=True)      # produces values
T_contra = TypeVar("T_contra", contravariant=True)  # consumes values
T = TypeVar("T")                            # invariant (default)

class Producer(Generic[T_co]):
    def get(self) -> T_co: ...

class Consumer(Generic[T_contra]):
    def put(self, item: T_contra) -> None: ...

Python 3.12’s PEP 695 type parameter syntax removes the manual annotation entirely: the checker infers variance from how each parameter is used. This is the recommended modern form.

# Python 3.12+, PEP 695 — variance is inferred, not declared
class Producer[T]:
    def get(self) -> T: ...        # T used only in output → inferred covariant

class Consumer[T]:
    def put(self, item: T) -> None: ...  # T used only in input → inferred contravariant

The producer/consumer rule

A type parameter that only ever appears in output positions (return types) can be covariant. A parameter that only appears in input positions (parameter types) can be contravariant. A parameter that appears in both — like the element type of a mutable list — must be invariant, because it is read and written.

This is why list[T] is invariant but Sequence[T] (read-only) is covariant. Passing a list[Dog] where list[Animal] is expected would let the callee append a Cat, corrupting the original list — so the analyzer rejects it.

# Python 3.11+, mypy 1.x
def add_animal(animals: list[Animal]) -> None:
    animals.append(Cat())          # legal for list[Animal]

dogs: list[Dog] = [Dog()]
add_animal(dogs)                   # mypy error: [arg-type]
# pyright: reportArgumentType — "list[Dog]" is not assignable to "list[Animal]"

Analyzer behaviour

mypy

mypy enforces variance strictly during assignment and argument checks. With explicit-variance TypeVars it also validates the declaration: if you mark a parameter covariant but use it in an input position, mypy raises [misc] — “Cannot use a covariant type variable as a parameter”. Run under mypy strict mode to surface these at CI time.

# Python 3.8+, mypy 1.x
from typing import TypeVar, Generic
T_co = TypeVar("T_co", covariant=True)

class Box(Generic[T_co]):
    def set(self, value: T_co) -> None: ...  # mypy error: [misc]
    # "Cannot use a covariant type variable as a parameter"

pyright

pyright performs the same soundness checks and, for PEP 695 classes, reports an inferred-variance mismatch as reportGeneralTypeIssues. pyright is generally faster to flag variance violations in deeply nested generics; the divergences are catalogued in pyright vs mypy.

Strictness tuning

Variance errors cannot be selectively disabled without losing soundness, but you can scope checks during incremental adoption with per-module overrides:

# pyproject.toml — relax a legacy module while you fix variance violations
[[tool.mypy.overrides]]
module = "legacy.collections_shim"
disable_error_code = ["arg-type"]

Prefer fixing the root cause: switch a mutable parameter type to its read-only protocol (Sequence, Mapping, Iterable) so the parameter becomes legitimately covariant.

Debugging false positives

A frequent “false positive” is really a genuine soundness error: passing dict[str, Dog] where dict[str, Animal] is expected. dict values are invariant. If the function never mutates the dict, accept Mapping[str, Animal] instead — a covariant, read-only type — and the error disappears correctly.

# Python 3.11+, mypy 1.x — fix by accepting a read-only Mapping
from collections.abc import Mapping

def describe(registry: Mapping[str, Animal]) -> None: ...  # covariant in the value type
describe({"rex": Dog()})           # now accepted

Common pitfalls

  • Marking a mutable container covariant: A covariant TypeVar on a class with a setter is unsound; mypy rejects the declaration with [misc]. Use invariance for anything writable.
  • Expecting list to behave like Sequence: list[Dog] is not a list[Animal]. Annotate read-only parameters as Sequence/Iterable to gain covariance safely.
  • Manually setting covariant=True under PEP 695: The new syntax infers variance; adding the old keyword is an error. Let the checker decide.
  • Confusing variance with subtyping of the parameter: Dog <: Animal says nothing on its own about Container[Dog] vs Container[Animal] — only the container’s variance does.

FAQ

Why is list invariant but tuple covariant? tuple is immutable, so its element type appears only in output positions and can be covariant. list is mutable — the element type is both read and written — so it must be invariant.

Do I still need covariant=True in Python 3.12? No. Under PEP 695 the type checker infers variance from usage. The explicit keywords remain only for legacy TypeVar declarations on 3.8–3.11.

How do I make a function accept “a list of any animal subtype”? Type the parameter as Sequence[Animal] (covariant, read-only) rather than list[Animal], or make the function itself generic with a bounded TypeVar.

Back to Advanced Typing Patterns & Generics