Why list Is Invariant but Sequence Is Covariant
list[int] is not a list[float], even though int is compatible with float, because lists are mutable — treating one as the other would let you append a float into a list that someone else still reads as ints. That unsoundness is why list is invariant. Read-only Sequence and Iterable are covariant, so a Sequence[int] is a Sequence[float]. Type function parameters as Sequence (or Iterable) when you only read them.
Variance is the rule that decides whether Container[Sub] may stand in for Container[Super]. For mutable containers the answer is “no,” and the reason is entirely about writes. This page belongs to Variance and Type Parameters and works through the concrete unsound-append argument, the exact error codes, and the read-only fix.
list[int] where list[float] is expected would let a float leak back to the caller.The unsound-append argument
Assume, for contradiction, that list[int] were assignable to list[float]. A function taking list[float] is entitled to append a float. But the caller still holds the same object typed as list[int] and will read its elements as int. One shared mutation breaks the caller’s promise.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
def add_pi(values: list[float]) -> None:
values.append(3.14) # legal: a float list may receive a float
nums: list[int] = [1, 2, 3]
add_pi(nums) # mypy: [arg-type]
# error: Argument 1 to "add_pi" has incompatible type "list[int]"; expected "list[float]"
reveal_type(nums[3]) # statically int, but 3.14 at runtime — would be a lie
mypy rejects the call with [arg-type]; pyright rejects it as reportArgumentType. The checker forbids the first step so the corrupting append can never happen. This is the whole justification for list being invariant in its element type.
Invariance means both directions fail
Invariance is symmetric. Not only is list[int] not a list[float], but list[float] is not a list[int] either — the second would let a reader pull an int slot out of a list that actually holds floats.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
def totals() -> list[int]:
data: list[float] = [1.0, 2.0]
return data # mypy: [return-value]
# error: Incompatible return value type (got "list[float]", expected "list[int]")
Neither substitution is safe, so a mutable generic accepts only an exact element-type match.
The fix: accept a read-only Sequence
If a function only reads its argument, annotate the parameter with an immutable, covariant type. Sequence (from collections.abc) has no append, so the write that made the substitution unsound is simply not part of the interface — and the checker can safely allow list[int] where Sequence[float] is expected.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Sequence
def mean(values: Sequence[float]) -> float: # read-only, covariant
return sum(values) / len(values)
nums: list[int] = [1, 2, 3]
mean(nums) # OK — Sequence[int] is a Sequence[float]
Sequence[T] is declared covariant in T, so Sequence[int] is a subtype of Sequence[float]. Iterable[T] is covariant too and is the right choice when you only iterate once and never index. For the fuller picture of these ABCs, see typing collections and collections.abc.
add_pi(nums) would execute fine and silently append 3.14 to your int list; nothing raises. The checker's [arg-type] is your only warning that the mutation corrupts the caller's view. Annotating parameters as Sequence makes the safe intent explicit.
Common mistakes
- Typing a read-only parameter as
list[...]: callers with atupleor a differently-typed list are rejected with[arg-type]even though you never mutate. UseSequenceorIterableto widen what you accept. - Expecting
list[int]to satisfylist[float]: it never will —[arg-type]in mypy,reportArgumentTypein pyright. Change the parameter toSequence[float], or convert with[float(x) for x in nums]if you truly need a float list. - Returning a
list[float]from a-> list[int]function:[return-value]. Invariance blocks the return; build the exact element type you promise. - Assuming all generics are invariant: they are not.
Sequence,Iterable,Mapping(in its value) andfrozensetare covariant;Callableis contravariant in its arguments. Only mutable containers likelist,set, anddictare invariant.
FAQ
Why can’t the checker just allow the substitution when I promise not to append?
Static analysis cannot see a runtime promise; it can only see the type. As long as the parameter is list[float], any code in that function (or anything it hands the list to) is allowed to append. The type system has to assume the whole capability is available, so it rejects the call up front. Narrow the type to Sequence[float] to encode “read-only” in a way the checker can trust.
Is tuple invariant like list?
No. tuple is immutable, so tuple[int, ...] is covariant and is a tuple[float, ...]. That is another reason to prefer immutable containers in interfaces: they compose with subtyping the way intuition expects, whereas list does not.