skip to content

Builtin and ABC Generics

Spelling containers in signatures — list[int], dict[str, int], tuple forms — and choosing abstract collections.abc types for parameters. Interviewers check whether you still reach for typing.List.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

How do `list[int]` and `typing.List[int]` differ in Python 3.14?

level: juniorimportance: must knowfreq 62%

answer

  1. Two spellings, one of them historical
  2. Builtins could not be subscripted before 3.9
  3. PEP 585 made the classes themselves generic
  4. Deprecated, still importable, no runtime warning

basics

~20 s

Both say 'a list of ints' to a type checker. Since PEP 585 in Python 3.9 the builtin list is subscriptable itself, so list[int] is the current spelling and typing.List is a deprecated alias kept only for older code.

solid answer

~40 s

They are interchangeable to a type checker, and only one of them is current. Before 3.9 the builtins were not subscriptable, so `typing` shipped capitalised aliases — `List`, `Dict`, `Set`, `Tuple`, `FrozenSet` — purely so annotations could be written at all. **PEP 585 in Python 3.9** gave the builtins and the `collections.abc` classes a subscript hook, so `list[int]`, `dict[str, int]`, `set[str]` and `collections.abc.Sequence[str]` now work directly, and the `typing` aliases were deprecated the same release. On 3.14 they still exist and emit no runtime warning — PEP 585 promised no removal for at least five years — but new code should not use them, and most linters flag them. At runtime the two are different objects: `typing.List[int] == list[int]` is `False`, even though every checker treats them as the same type.

code

pycon · 9 lines
pycon
>>> import typing
>>> list[int]
list[int]
>>> type(list[int])
<class 'types.GenericAlias'>
>>> typing.List[int]
typing.List[int]
>>> typing.List[int] == list[int]
False

go deeper

for a junior

Be ready to write modern signatures without thinking: list[int], dict[str, int], set[str]. Know that the capitalised typing.List spelling is the old way, kept working for old code, and that PEP 585 in 3.9 is where the change happened.

for a middle

Explain why the duplicate existed — builtins were not subscriptable before 3.9 — and that the deprecation is documentation-level, with no runtime warning to catch it. Be able to list the mechanical rewrite, including typing.Optional becoming | None in 3.10.

for a senior

Own the migration story for a real codebase: which typing names go away, which stay because they are not duplicates, and why mixed spellings hurt any code that inspects annotation objects. Point at the linter rule rather than relying on people to remember.

for a principal

Frame it as a house-style decision with a minimum-interpreter constraint attached. Decide once, encode it in the lint configuration so it is not re-argued in review, and treat the same way any future syntax-level replacement of a library workaround.

### Why there were ever two spellings Type hints arrived in Python 3.5, but the builtin container classes were not subscriptable then: writing `list[int]` at runtime raised `TypeError: 'type' object is not subscriptable`. So the `typing` module shipped a parallel set of capitalised aliases — `typing.List`, `typing.Dict`, `typing.Set`, `typing.FrozenSet`, `typing.Tuple`, `typing.Type` — whose only job was to be subscriptable stand-ins for the real classes. A whole generation of code learned to import them, and a whole generation of interviewers learned to check whether you still do. ### What PEP 585 changed in 3.9 PEP 585, landed in **Python 3.9**, gave the builtin containers and the `collections.abc` classes a class-level subscript hook (`__class_getitem__`), so the classes themselves became generic. From 3.9 onward you write: ```python def reprice(prices: dict[str, int], skus: set[str]) -> list[tuple[str, int]]: ... ``` The same release deprecated every `typing` alias that PEP 585 made redundant. The deprecation is documentation-level: the aliases are still importable on 3.14, still work, and raise **no** `DeprecationWarning` at runtime, because PEP 585 explicitly promised not to remove them for at least five years and no removal has been scheduled. That is why old code keeps running and why the habit persists. ### The two objects are not the same object To a type checker `list[int]` and `typing.List[int]` denote the identical type. At runtime they are different values: `list[int]` is a `types.GenericAlias`, a small object remembering the class and its parameters, while `typing.List[int]` is a `typing`-module alias object, and they do not compare equal. Code that pattern-matches on annotation objects therefore has to handle both shapes — one more reason to converge on a single spelling in a codebase rather than mixing them file by file. ### What still lives in `typing` PEP 585 replaced only the aliases that duplicate a real runtime class. Plenty of `typing` is not duplication and stays: `typing.Any`, `typing.Protocol`, `typing.Literal`, `typing.Final`, `typing.ClassVar`, `typing.TypeVar`, `typing.NamedTuple`, `typing.TypedDict`, `typing.cast`, `typing.NoReturn`. A second family was superseded by syntax rather than by a builtin: `typing.Optional[X]` and `typing.Union[X, Y]` are written `X | None` and `X | Y` since **Python 3.10** (PEP 604). And `typing.Callable` has a non-deprecated home too — `collections.abc.Callable` — which PEP 585 made subscriptable alongside the rest. ### A subtlety that trips people: annotation position versus runtime position On 3.7 and 3.8, `from __future__ import annotations` (PEP 563) turned annotations into strings that were never evaluated, so `def f(x: list[int]) -> None` *looked* fine even though `list[int]` would have raised if actually executed. That worked only in annotation position. Assigning a type alias — `Rows = list[tuple[str, int]]` — or calling `typing.cast(list[int], value)` evaluates the expression for real, and on 3.8 that still failed. The lesson survives the version: an annotation is not always evaluated, but a plain expression always is. On **Python 3.14** the lazy behaviour became the default under PEP 649/749: annotations are compiled into a separate function and evaluated only when something asks for them, so a bad annotation does not blow up at definition time at all. `def pick(rows: list[NotDefinedYet]) -> None: pass` defines cleanly; the `NameError` appears only when the annotations are read. That makes an unused wrong annotation quieter than it used to be, which is an argument for running a static checker rather than trusting import to catch it. ### Migrating an existing codebase The change is mechanical and safe: drop the capitalised import, lowercase the name. `List` becomes `list`, `Dict` becomes `dict`, `Set` becomes `set`, `FrozenSet` becomes `frozenset`, `Tuple` becomes `tuple`, `Type` becomes `type`, `typing.Deque` becomes `collections.deque`, and the `typing` re-exports of the abstract collections (`typing.Sequence`, `typing.Mapping`, `typing.Iterable`) become the real classes from `collections.abc`. Automated tooling in the linting ecosystem does the rewrite; the only real constraint is the minimum interpreter version you support, and 3.9 has been end-of-life for long enough that on a 3.14 target there is nothing left to weigh. ### What an interviewer is actually testing Not trivia about PEP numbers. They are checking whether your habits track the language: whether you know why the duplicate spelling exists, that it is deprecated rather than broken, that no runtime warning will tell you, and that the fix is a one-line lowercase. Saying "`typing.List` is needed for type checking to work" is the answer that ends the line of questioning badly.

  • Does importing typing.List emit a DeprecationWarning on 3.14?
    No. The deprecation is documented but not enforced at runtime: `typing.List[int]` evaluates silently on 3.14. PEP 585 committed to leaving the aliases in place for at least five years after 3.9 and no removal release has been announced, so nothing in the interpreter will nudge you. A static checker or a linter rule is what actually flags the old spelling.
  • How would you spell an optional list of SKU strings today?
    `list[str] | None`. PEP 604 in Python 3.10 made `X | Y` valid in annotations, which retires both `typing.Optional[list[str]]` and `typing.Union[list[str], None]`. The `|` form is also a real runtime expression outside annotations, and `isinstance(x, int | str)` is accepted from 3.10 onward — unlike a parameterised generic such as `list[int]`, which `isinstance` rejects.
  • Why did some pre-3.9 code write list[int] with `from __future__ import annotations`?
    Because PEP 563 turned annotations into unevaluated strings, so the interpreter never executed `list[int]` and the fact that builtins were not subscriptable on 3.7/3.8 did not matter. The trick covered annotation position only: a type alias assignment or a `typing.cast` call evaluates the expression for real and still failed. On 3.14 annotations are lazy by default anyway.

The typing aliases are the old street name on a building that was renumbered years ago: the post still arrives, but nobody prints it on new stationery.

saying these in an interview costs you the question

  • Says typing.List is required for a type checker to work
  • Thinks list[int] validates elements at runtime
  • Claims typing.List was removed in Python 3.9
  • Believes list[int] needs a __future__ import on 3.14
  • Assumes typing.List[int] == list[int] compares True
  • Mixes both spellings in one codebase without noticing

context

open as a page

Why does `isinstance(x, list[int])` raise TypeError at runtime?

level: middleimportance: should knowfreq 33%

basics

~20 s

Because list[int] is not a class: it is a types.GenericAlias describing a class plus its parameters, and CPython refuses to check one. Annotations are never enforced at runtime, so nothing verifies the element types for you.

open as a page

How does `tuple[int, ...]` differ from `tuple[int, str]` in a signature?

level: middleimportance: should knowfreq 44%

basics

~10 s

tuple[int, ...] is a homogeneous tuple of any length, including empty. tuple[int, str] is exactly two elements: an int then a str. The literal ellipsis means 'more of the same', never 'and whatever else'.

open as a page

Why annotate a parameter `collections.abc.Mapping[str, int]` rather than `dict[str, int]`?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Mapping is the abstract interface, so the parameter accepts any mapping a caller already has, not only a real dict. It also states that the function will not mutate the argument, because the abstract Mapping has no item assignment.

open as a page