Most real data is collections; precise element types catch the bugs that matter.
Since Python 3.9, built-in collection types accept type arguments directly:
list[int],set[str],dict[str, float],tuple[int, str]for a fixed-length pair,tuple[int, ...]for any length of ints.
The old typing.List, typing.Dict aliases are deprecated; use the built-ins.
For parameters, prefer abstract types from collections.abc that describe only what you need:
Iterable[T]if you just loop over it,Sequence[T]if you index or takelen()(lists, tuples, strings),Mapping[K, V]for read-only dict-like access,MutableSequence,MutableMappingif you modify it.
This makes functions accept more inputs (a tuple, a generator, a custom class) and documents that you don't mutate the argument. For return types, be concrete (list[str]) so callers know exactly what they get.
An empty literal needs a hint, or the checker can't know the element type: names: list[str] = [].
Sequence[str] has a trap: a single str is itself a sequence of strings, so passing "abc" where a list of names is expected type-checks.
Example
from collections.abc import Iterable, Mapping, Sequence
def total(prices: Iterable[float]) -> float:
return sum(prices)
def first_last(items: Sequence[str]) -> tuple[str, str]:
return items[0], items[-1]
def lookup(table: Mapping[str, int], key: str) -> int:
return table.get(key, 0)
print(total([1.5, 2.5]), total(p for p in (1.0, 2.0))) # 4.0 3.0
print(first_last(("a", "b", "c"))) # ('a', 'c')
print(lookup({"x": 1}, "y")) # 0
scores: dict[str, list[int]] = {}
scores.setdefault("ada", []).append(95)
point: tuple[int, int] = (3, 4)
coords: tuple[float, ...] = (1.0, 2.0, 3.0)
print(scores, point, coords)Remember
- Use list[int], dict[str, int], tuple[int, ...] directly
- Accept abstract types (Iterable, Sequence, Mapping); return concrete ones
- Annotate empty collections
Watch out for
- Typing a parameter as list[str] when any iterable would do, forcing callers to convert.