Sequence vs list in Function Parameters
TL;DR — For a parameter you only read, annotate Sequence[T], not list[T]. list is invariant, so list[T] rejects list[subtype] callers and refuses tuples and strings outright. Reach for MutableSequence[T] only when the function actually calls append, sort, or slice assignment on the argument.
A parameter annotation is a promise about what the function does with the value. list[int] promises two things a reader may not intend: “I might mutate this” and “it must be exactly a list.” Both promises quietly shrink your caller set. Sequence[int] promises only “I will read this by index or iteration,” which is what most functions actually do.
list is invariant, and that costs you callers
list is invariant in its element type. That means list[bool] is not accepted where list[int] is expected, even though every bool is an int. The reason is soundness: a function holding a list[int] could append a plain 2, which would be an invalid element of the caller’s list[bool]. Invariance is not an implementation quirk of mypy — it is required by PEP 483’s typing theory for any mutable container, because a mutable container is used in both a read (covariant) and a write (contravariant) position, and the only variance compatible with both is invariance.
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
def sum_scores(scores: list[int]) -> int:
return sum(scores)
flags: list[bool] = [True, True, False]
sum_scores(flags)
# mypy: Argument 1 to "sum_scores" has incompatible type "list[bool]";
# expected "list[int]" [arg-type]
# pyright: reportArgumentType
The function never mutates scores, so the rejection is a false alarm produced entirely by the annotation. This is the same invariance rule covered in depth under variance and type parameters. Pyright words it slightly differently — Argument of type "list[bool]" cannot be assigned to parameter "scores" of type "list[int]" ... "list[bool]" is incompatible with "list[int]" — but the diagnosis is the same. Note that bool subclassing int is a real language fact (PEP 285), which is exactly why this example is not contrived: the subtype relationship holds, yet invariance still blocks it. The same rule bites list[int] against list[float]: even though the numeric tower (PEP 484 treats int as compatible with float), list[int] is not a list[float].
Invariance is symmetric across parameter and return positions, because it is a property of the list type itself rather than of where the type appears. A function annotated -> list[float] therefore cannot return a list[int] you built locally; you either annotate the return list[float] and build with float literals, widen the return to a covariant Sequence[float], or add an explicit cast. This is a common source of [return-value] errors during refactors and one more reason read-only positions gravitate toward Sequence.
Sequence is covariant and accepts the whole family
Sequence[T] from collections.abc is covariant in T, and it is satisfied by any indexable, sized, iterable object. Covariance is sound here precisely because Sequence has no mutation methods in its protocol: with no write position, the element type appears only in return positions (__getitem__ returns T), and a return-only type parameter can safely be covariant. Switch the annotation and every earlier caller type-checks:
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Sequence
def sum_scores(scores: Sequence[int]) -> int:
return sum(scores)
sum_scores([1, 2, 3]) # list[int] ✓
sum_scores((10, 20)) # tuple[int, ...] ✓
sum_scores(range(5)) # range ✓
sum_scores([True, False]) # list[bool] ✓ (covariant)
You lose nothing: sum, len, indexing, slicing, and iteration are all part of the Sequence contract. You only give up mutation methods — which you were not calling. One import note: before Python 3.9 you had to write from typing import Sequence; since 3.9 (PEP 585) the canonical home is collections.abc, and on 3.9+ importing it from typing triggers ruff UP035 (typing.Sequence is deprecated). The typing alias still works at runtime and is not scheduled for removal, but the linter will nudge you to collections.abc.Sequence. Both mypy and pyright treat the two spellings as the same type. If you need to subscript a collections.abc ABC in a runtime context on Python 3.8, you must either keep the typing alias or add from __future__ import annotations so the subscription is never evaluated — see Mapping vs dict for read-only arguments for the same pattern on mappings.
Concretely, the read-only Sequence[T] family is satisfied by list, tuple, and range, by str (as a Sequence[str]), and by bytes, bytearray, and memoryview (as Sequence[int]), plus anything registered via collections.abc.Sequence.register(...). A frequent surprise is that a numpy.ndarray is not a Sequence to the type checker — NumPy does not register its arrays with collections.abc.Sequence, so a Sequence[float] parameter rejects an ndarray at the call site even though it indexes and iterates fine at runtime; accept numpy.typing.NDArray or a small Protocol there instead. There is also a subtlety with tuples: a homogeneous variadic tuple[int, ...] is a Sequence[int], but a fixed-length heterogeneous tuple[int, str] is not a Sequence[int] — it is a Sequence[int | str], because the element type of a heterogeneous tuple is the union of its members, and mypy will surface that union if you pass one where Sequence[int] is expected.
MutableSequence when you really do mutate
When the function does mutate the argument, say so with MutableSequence[T]. It advertises append, extend, insert, __setitem__, __delitem__, pop, remove, reverse, and slice assignment, and it correctly rejects immutable callers like tuples at the call site rather than at a runtime AttributeError. This is the “just enough capability” middle rung: broader than list[T] (it also admits collections.deque and any custom MutableSequence subclass) but narrower than Sequence[T] (it excludes the read-only tuple, range, and str).
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import MutableSequence
def dedupe_in_place(items: MutableSequence[str]) -> None:
seen: set[str] = set()
write = 0
for value in items:
if value not in seen:
seen.add(value)
items[write] = value # needs __setitem__
write += 1
del items[write:] # needs slice deletion
dedupe_in_place(("a", "b"))
# mypy: Argument 1 has incompatible type "tuple[str, str]";
# expected "MutableSequence[str]" [arg-type]
Concrete list[str] would also work here, but MutableSequence[str] documents intent — “I mutate, but any mutable sequence will do” — and still admits things like collections.deque. Be aware that MutableSequence is itself invariant, for the same soundness reason as list: it has __setitem__ and append, both of which take a T argument, so MutableSequence[bool] is rejected where MutableSequence[int] is expected. Passing a deque[str] here type-checks because collections.deque is registered as a virtual subclass of MutableSequence; on Python 3.9+ you can subscript it directly (deque[str]), while on 3.8 you would write typing.Deque[str]. If you only ever append (never index or slice), a hand-written Protocol with a single append method expresses the requirement even more precisely — but in practice MutableSequence[T] is the idiomatic annotation for any in-place list rewrite.
Beyond list and deque, the standard library offers two more concrete MutableSequence implementations worth knowing: bytearray is a MutableSequence[int], and array.array behaves like one for its element type. Both support in-place __setitem__ and slice assignment, so a function typed MutableSequence[int] can rewrite a bytearray buffer in place without first copying it to a list. The immutable siblings — bytes, str, tuple, range, and a read-only memoryview — are deliberately excluded, which is precisely the call-site protection you want: passing a bytes object to a function that calls .append() is caught statically as [arg-type] rather than failing at runtime with AttributeError: 'bytes' object has no attribute 'append'.
A quick decision rule
Walk the body of the function and note which operations you perform on the parameter. If it is only iteration, len, indexing, or slicing, the answer is Sequence[T]. If you also mutate — append, sort, items[i] = x, del items[i] — the answer is MutableSequence[T] (or concrete list[T] when you specifically want to hand the caller something list-shaped). You almost never need list[T] on a parameter; it appears mostly out of habit. Note one subtlety: list.sort() is a list-specific method, not part of the MutableSequence protocol, so a function that calls items.sort() needs concrete list[T]; a function that would work on any mutable sequence should call the built-in sorted(items) (which returns a new list) or reassign via slice.
# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Sequence
class OrderRepository:
def total_amount(self, line_items: Sequence[int]) -> int:
return sum(line_items) # read-only → Sequence
def append_audit(self, log: list[str], entry: str) -> None:
log.append(entry) # mutates a caller-owned list → list is honest
The split reads as documentation: total_amount cannot alter its input, append_audit announces that it will. The same discipline scales to return types, though the trade-off flips — see Sequence versus list for return types below. When in doubt on a parameter, start at the most permissive Sequence[T] and let the type checker force you narrower: if mypy reports "Sequence[T]" has no attribute "append" [attr-defined], that is the signal to move up exactly one rung to MutableSequence[T], not to jump straight to list[T].
The collections.abc hierarchy also offers finer-grained rungs below Sequence for when you need even less. Collection[T] is sized, iterable, and membership-testable but not indexable; Container[T] supports only in; and Reversible[T] supports reversed(). If a function only checks membership with x in items and never indexes, Collection[T] is more honest than Sequence[T] and additionally accepts set and frozenset, which are not sequences at all. Choosing the narrowest ABC that still covers every operation you perform is simply the general form of the whole Sequence-over-list argument: program to the interface, not the implementation.
The str gotcha
str satisfies Sequence[str]: it is indexable, sized, and iterates into one-character strings. So a parameter typed Sequence[str] silently accepts a bare string and treats it as a sequence of characters — no analyzer error whatsoever. This is uniquely nasty for str because a string of characters is itself a valid Sequence[str] — the element type and the container type coincide — so there is no type-level distinction for a checker to catch. (The analogous bytes/Sequence[int] confusion works the same way: iterating bytes yields int, so a bytes value satisfies Sequence[int].)
# Python 3.9+, mypy 1.10 — no error is raised here
from collections.abc import Sequence
def join_tags(tags: Sequence[str]) -> str:
return ",".join(tags)
join_tags("prod") # returns "p,r,o,d" — accepted, silently wrong
join_tags("prod"), because str genuinely is a Sequence[str] — the bug is semantic, not a type error. If passing a lone string is a real hazard, validate at runtime (if isinstance(tags, str): raise TypeError(...)) or annotate the parameter list[str] so a bare str is rejected as [arg-type]. Static typing cannot express "any sequence except str."
There is no NotStr type, and proposals for a str-excluding sequence type have not been accepted, so the practical choices are: (1) tighten the annotation to list[str] or tuple[str, ...] when you specifically want to bar strings — this is the one place a concrete list[str] parameter earns its keep; (2) add a runtime isinstance(tags, str) guard at the top of the function; or (3) accept the risk when the function is internal and never called with a raw string. The gotcha is worst at API boundaries where callers are untrusted. For collections you only iterate (never index), typing the parameter Iterable[str] does not help — str is an Iterable[str] too — so the runtime guard remains the only reliable defense.
Common mistakes
These five errors account for nearly every Sequence/list annotation problem you will see in review, and each maps to a single operation the body performs; the matrix below summarizes which annotation each operation demands. The rule behind every row is identical: annotate the least-capable interface that still supports what you actually do with the parameter, then let the type checker escalate you one rung at a time when a method turns up missing.
list[T]on a read-only parameter. Rejectslist[subtype]and every non-list caller with mypy[arg-type]/ pyrightreportArgumentType. UseSequence[T].Sequence[T]where you call.append(). mypy reports"Sequence[T]" has no attribute "append"[attr-defined]; pyright reportsreportAttributeAccessIssue. Upgrade toMutableSequence[T].- Forgetting
stris a sequence.Sequence[str]accepts a lone string with no error; guard at runtime if that is invalid. - Returning
Sequence[T]then mutating the result. The caller sees a read-only type and mypy blocks.append()on it with[attr-defined]; returnlist[T]when callers must mutate. - Importing
Sequencefromtypingon 3.9+. Works, but ruffUP035flags the deprecated alias; import fromcollections.abcper PEP 585.
Underlying all five is one habit worth internalizing: reach for the concrete list[T] on a parameter only when you call a list-specific method or genuinely need to hand back a mutable list; the rest of the time Sequence[T] (read) or MutableSequence[T] (write) both documents intent and maximizes the set of callers your function accepts. The same reasoning generalizes to every container — prefer Mapping over dict, Iterable over list, Collection over set — whenever the body only exercises the smaller interface.
FAQ
Does using Sequence slow anything down at runtime?
No. Annotations are erased at runtime under normal execution; Sequence[int] and list[int] produce identical bytecode for the function body. The difference is purely what the type checker accepts.
Should return types also be Sequence?
Only if you want to stop callers from mutating the result. For a return, prefer the concrete list[T] when callers legitimately need to append or sort; use Sequence[T] to keep the backing store swappable.