Why list[Dog] Is Not list[Animal] but Sequence[Dog] Is
Mutable collections like list and dict are invariant: list[Dog] is not assignable to
list[Animal], because the callee could append a Cat and corrupt your list. Read-only abstract
types — Sequence, Mapping, Iterable, and tuple — are covariant, so Sequence[Dog] is a
Sequence[Animal]. Fix [arg-type] / reportArgumentType errors by annotating parameters you only
read with those abstract types instead of the concrete mutable ones.
This is the single most common generics error a static analyzer reports: you pass a list[Dog] to a
function annotated list[Animal] and mypy rejects it. The reason is
variance — specifically the
producer/consumer rule applied to real collection types. This guide explains why each standard
collection is invariant or covariant and shows the one-line fix that makes the error disappear
correctly, without suppressing a genuine soundness problem. It assumes Python 3.11+ and mypy 1.x or
pyright.
Context: the producer/consumer rule
A collection’s element type can be covariant only if the collection never lets you write that
element. A list is both readable and writable, so its element type sits in input and output
positions and must be invariant. A Sequence exposes only reads (__getitem__, iteration), so its
element type is output-only and can be covariant. The same logic splits dict (invariant) from
Mapping (covariant in its value type). This is PEP 483’s variance rule applied to the standard
library.
Step 1 — Reproduce the invariance error
Passing a list[Dog] where list[Animal] is wanted is rejected, even though Dog is a subtype of
Animal.
# Python 3.11+, mypy 1.x / pyright
class Animal: ...
class Dog(Animal): ...
class Cat(Animal): ...
def add_stray(animals: list[Animal]) -> None:
animals.append(Cat()) # legal for a list[Animal]
dogs: list[Dog] = [Dog()]
add_stray(dogs) # mypy error: [arg-type]
# pyright: reportArgumentType — "list[Dog]" is not assignable to "list[Animal]"
The error is correct: add_stray would insert a Cat into something the caller believes is a
list[Dog]. Invariance prevents that.
Step 2 — Switch a read-only parameter to Sequence
If the function only reads the collection, annotate it as Sequence[Animal]. Sequence is
covariant, so Sequence[Dog] is accepted as a Sequence[Animal].
# Python 3.11+, mypy 1.x — read-only → covariant
from collections.abc import Sequence
def describe(animals: Sequence[Animal]) -> None:
for a in animals:
print(type(a).__name__) # only reads — never appends
dogs: list[Dog] = [Dog()]
describe(dogs) # accepted: Sequence[Dog] <: Sequence[Animal]
A list is a Sequence, so callers pass their concrete list unchanged — you only relaxed the
parameter type.
Step 3 — Apply the same fix to mappings
dict is invariant in both its key and value type. If a function only looks values up, accept a
Mapping[str, Animal], which is covariant in the value type.
# Python 3.11+, mypy 1.x
from collections.abc import Mapping
def first_name(registry: Mapping[str, Animal]) -> str:
return next(iter(registry)) # read-only
shelter: dict[str, Dog] = {"rex": Dog()}
first_name(shelter) # accepted via Mapping covariance
Step 4 — Know which collections are already covariant
tuple and the read-only abstract base classes need no fix — they are covariant out of the box
because they expose no mutators.
# Python 3.11+, mypy 1.x / pyright
def total(values: tuple[Animal, ...]) -> int:
return len(values)
dogs: tuple[Dog, ...] = (Dog(), Dog())
total(dogs) # accepted: tuple is covariant (immutable)
list[Dog] and
list[Animal] are the same list class — Python performs no element-type
checking on append or assignment. The analyzer rejects the assignment to stop a bug it
can prove is possible (appending a Cat), but nothing enforces it while the program runs.
Switching to Sequence changes only what the checker accepts, not runtime behaviour.
Edge cases
- A function that both reads and writes: Keep the invariant
list[Animal]. The error is real — you cannot soundly acceptlist[Dog]if you ever append a non-Dog. setandfrozenset:setis invariant (mutable);frozensetis covariant. The mutable/ immutable split is the deciding factor again.- Covariant return, invariant field: A method may return
Sequence[Animal]covariantly while a storedlist[Animal]attribute stays invariant — annotate the boundary, not the storage.
Common mistakes
- Annotating read-only parameters as
list: This forces invariance and rejects valid subtype collections with[arg-type]/reportArgumentType. UseSequence/Iterable/Mappingfor read-only parameters. - Suppressing the error with
# type: ignore: On a function that mutates, the error is genuine; ignoring it lets aCatinto alist[Dog]. Fix the type, do not silence[arg-type]. - Expecting
dict[str, Dog]to satisfydict[str, Animal]:dictvalues are invariant. UseMapping[str, Animal]if the function only reads, or you will hit[arg-type].
FAQ
Why is tuple covariant but list is not?
tuple is immutable, so its element type appears only in output positions and can be covariant.
list is mutable — read and written — so it must be invariant.
Does using Sequence slow anything down?
No. Sequence is an abstract base class for typing only; you still pass the same concrete list at
runtime, with identical performance.