Typing Collections and collections.abc in Modern Python

Annotating collections well is where most type-hint hygiene pays off: pick Sequence[str] and your function accepts lists, tuples, and any read-only sequence; pick list[str] and you have quietly rejected half your callers. This cluster maps the built-in generics against the collections.abc abstract base classes, explains PEP 585, and shows how variance decides which annotation is safe on a parameter versus a return.

collections.abc protocol hierarchy for annotations Iterable is the root; Collection adds container and sized; Sequence and Mapping refine Collection; MutableSequence and MutableMapping add write methods; list and dict are the concrete leaves. Iterable[T] __iter__ only Collection[T] + len, in, sized Sequence[T] indexed, covariant Mapping[K, V] keyed, V covariant MutableSequence[T] list — invariant MutableMapping[K, V] dict — invariant
Read-only abstract types near the top accept more callers; mutable concrete leaves at the bottom guarantee write methods but are invariant.

Built-in generics replace typing.List (PEP 585)

Since Python 3.9, the standard containers subscript directly: list[str], dict[str, int], set[bytes], tuple[int, ...]. PEP 585 deprecated the aliased typing.List, typing.Dict, and typing.Set — they still work but exist only for backward compatibility. Ruff’s UP006 rewrites List[str] to list[str] automatically, and UP035 flags the deprecated imports.

# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
def load_records(rows: list[dict[str, int]]) -> set[str]:
    return {str(row["id"]) for row in rows}

# ruff UP006: use `list` not `typing.List`
# ruff UP035: `typing.Dict` is deprecated, import removed

On Python 3.7 and 3.8 the bare list[str] form only works inside a string annotation, which is what from __future__ import annotations gives you. If a library reads annotations at runtime — a dataclass calling get_type_hints(), for example — subscripting a builtin without that future import raises TypeError before 3.9.

collections.abc: the abstract vocabulary

collections.abc supplies the abstract base classes you annotate against when you care about capability, not concrete class. The ones you reach for daily:

  • Iterable[T] — supports for iteration (__iter__).
  • Iterator[T] — supports next() and single-pass consumption (__next__).
  • Collection[T] — iterable, sized, and supports in.
  • Sequence[T] — indexable and reversible; str, tuple, list, range all qualify.
  • Mapping[K, V] — keyed lookup without mutation; dict and MappingProxyType qualify.
  • MutableSequence[T] / MutableMapping[K, V] — add append/__setitem__ and friends.

Since Python 3.9 you import these from collections.abc, not typing; the typing.Sequence re-exports are deprecated aliases that ruff UP035 will flag. Use collections.abc for annotations and reserve the runtime ABCs (same objects) for isinstance checks.

# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Mapping, Sequence

def render(config: Mapping[str, str], order: Sequence[int]) -> str:
    return f"{len(config)} keys over {len(order)} items"

render({"env": "prod"}, (1, 2, 3))   # dict + tuple both accepted

typing.* versus collections.abc.*

The rule is mechanical: for a plain container annotation, import the runtime class from collections.abc. Keep typing only for constructs that have no collections.abc equivalent — Optional, Any, TypeVar, ClassVar, and pre-3.9 aliases. Mixing typing.Sequence with collections.abc.Mapping in one file is a common inconsistency; standardize on the collections.abc source so ruff has nothing to rewrite.

Runtime vs static analysis `collections.abc.Sequence` is subscriptable for annotations at runtime only on Python 3.9+. Before that, `from collections.abc import Sequence` imports the ABC fine, but `Sequence[int]` at runtime raises TypeError: 'ABCMeta' object is not subscriptable unless the annotation is deferred with from __future__ import annotations. Type checkers accept the subscript regardless of version.

Abstract to accept more, concrete to guarantee methods

The choice between abstract and concrete is a contract decision. On a parameter, prefer the widest abstract type that supplies the methods you actually call. If you only iterate and index, Sequence[str] lets callers pass a list, a tuple, or a custom read-only view. If you call .append(), you genuinely need MutableSequence[str] — or a concrete list[str].

On a return or attribute, be as concrete as the caller needs. Returning list[str] promises indexing and mutation; returning Sequence[str] promises only read access and lets you swap the backing store later. A repository method is the classic example:

# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import Iterable, Sequence

class OrderRepository:
    def bulk_insert(self, orders: Iterable[dict[str, int]]) -> None:
        for order in orders:      # only iterates → Iterable is widest safe type
            self._store.append(order)

    def recent(self, limit: int) -> Sequence[dict[str, int]]:
        return self._store[-limit:]   # read-only view for callers

Variance: why the wide types are safe on parameters

Read-only abstract types are covariant, and that is exactly why they accept subtypes. Sequence[T] and Mapping[K, V] (in V) are covariant, so Sequence[bool] is accepted where Sequence[int] is expected. list and dict are invariant: list[bool] is not a list[int], because a function that receives a mutable list[int] could append a plain int and corrupt the caller’s list[bool]. That single safety rule — mutability forces invariance — is why widening a parameter to Sequence/Mapping removes a whole class of [arg-type] errors. The deep explanation lives in why list is invariant but Sequence is covariant and the broader variance and type parameters guide.

# Python 3.9+, mypy 1.10 — invariance in action
def total(values: list[int]) -> int:
    return sum(values)

flags: list[bool] = [True, False]
total(flags)   # error: Argument 1 has incompatible type "list[bool]"; expected "list[int]"  [arg-type]
# Widen the parameter to Sequence[int] and the same call type-checks cleanly.

Sets, tuples, and the fixed-versus-variadic tuple

Two containers deserve their own note. set[T] and frozenset[T] have an abstract counterpart in AbstractSet[T] (covariant, read-only) and MutableSet[T]; annotate a read-only set parameter AbstractSet[T] for the same reason you prefer Sequence over list. Tuples are the exception to the “abstract is wider” rule because they carry positional type information. tuple[int, str, bytes] is a fixed three-element heterogeneous tuple; tuple[int, ...] is a homogeneous variadic tuple of any length. Widening a fixed tuple to Sequence[object] throws away the per-position types, so keep the explicit tuple[...] form when the arity matters — for example a coordinate tuple[float, float] returned from parse_response.

# Python 3.9+, checked with mypy 1.10 / pyright 1.1.370
from collections.abc import AbstractSet

def known_ids(ids: AbstractSet[int]) -> int:
    return max(ids)                     # accepts set and frozenset, read-only

def bounds() -> tuple[float, float]:
    return (0.0, 100.0)                 # fixed arity — do not widen to Sequence

Wire the collections.abc preference into CI so it stays consistent: enable ruff’s pyupgrade rules and let mypy strictness settings reject the untyped-container escapes that hide behind a bare list or dict.

Common mistakes

  • Annotating a parameter list[T] when you only read it. Rejects tuple and list-of-subtype callers with mypy [arg-type] / pyright reportArgumentType. Use Sequence[T].
  • Importing containers from typing. from typing import List, Dict triggers ruff UP035; the subscripted use triggers UP006. Import list/dict and collections.abc.
  • Passing a str where you meant a sequence of tokens. str is a Sequence[str], so Sequence[str] silently accepts a bare string and iterates it character by character — no analyzer error at all. Annotate list[str] or validate length when a real collection is required.
  • Using MutableMapping on a read-only argument. Forces callers to hand you a mutable dict and blocks MappingProxyType; downgrade to Mapping[K, V].

FAQ

Should I import Sequence from typing or collections.abc? From collections.abc on Python 3.9+. The typing.Sequence alias is deprecated (ruff UP035) and only kept for older code. Both point at the same runtime ABC.

Is tuple[int, ...] a Sequence[int]? Yes. Any tuple, list, range, or str-like object satisfies Sequence[T] when its elements match T. That is precisely why Sequence is the right parameter type for read-only access.

When is list[str] actually the correct annotation? When you mutate the argument in place (append, sort, slice assignment) or when a caller must be handed something they can mutate. For pure reads, Sequence[str] is strictly more permissive.

Back to Core Type Hints Fundamentals