Why does `isinstance(x, list[int])` raise TypeError at runtime?
answer
- The subscript builds an object, not a class
- What would the check have to visit
- The empty container defeats the answer
- Class test yes, parameters no
- Nothing enforces annotations at run time
basics
~20 sBecause 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.
solid answer
~40 sSubscripting a builtin container builds a `types.GenericAlias` — a small object recording the class and its parameters — and `isinstance` and `issubclass` explicitly reject it with `TypeError: isinstance() argument 2 cannot be a parameterized generic`. The reason is that the check would have to be untrue or expensive: an empty list matches every element type, checking a large one means walking every item, and for a lazily produced source it is impossible without consuming it. So Python checks the container class only. Use `isinstance(x, list)` for the shape and validate the elements explicitly if you need it. Note the contrast: unparameterised ABCs such as `collections.abc.Mapping` work with `isinstance`, and so does a union like `int | str`.
code
pycon · 13 lines>>> import types
>>> type(list[int])
<class 'types.GenericAlias'>
>>> isinstance([1, 2], list[int])
Traceback (most recent call last):
...
TypeError: isinstance() argument 2 cannot be a parameterized generic
>>> isinstance([1, 2], list)
True
>>> isinstance(1, int | str)
True
>>> list[int]([1, "two"])
[1, 'two']go deeper
Remember that type hints are not enforced when the program runs, and that isinstance takes a class: isinstance(x, list) is fine, isinstance(x, list[int]) raises TypeError. Knowing the error exists is enough at this level.
Explain the object involved - subscripting builds a types.GenericAlias, not a class - and why the check is refused: an empty container matches everything, a large one costs a full traversal, and a lazily produced one cannot be inspected at all.
Draw the line between static and runtime guarantees, and show the practical fallback: a class check plus explicit element validation whose traversal cost you accept knowingly, at the boundary where untrusted data enters rather than everywhere.
Decide where runtime validation belongs at all: which boundaries pay for it, which rely on the static checker in CI, and how much per-request traversal that policy costs. The tradeoff is confidence against latency, and it should be set once for the system.
### What the subscript actually builds `list[int]` is an ordinary expression, evaluated when it runs. It calls the class-level subscript hook that PEP 585 added in **Python 3.9** and produces a `types.GenericAlias`: a lightweight object remembering the origin class (`list`) and the parameters (`int`). It is cheap, it compares equal to another `list[int]`, and its `repr` is `list[int]`. What it is *not* is a class — it is a description of one. ### Why isinstance refuses it Give that object to `isinstance` and CPython raises immediately: ```pycon >>> isinstance([1, 2], list[int]) Traceback (most recent call last): ... TypeError: isinstance() argument 2 cannot be a parameterized generic ``` `issubclass` behaves the same. The refusal is deliberate rather than a missing feature, and the reasons stack up: **It could not be answered honestly.** Is `[]` a `list[int]`? Every empty list matches every element type, so the answer is yes for `list[str]` too — a check whose result does not distinguish the thing being asked about. **It would break the cost model.** `isinstance` is expected to be a cheap constant-time class test. Verifying `list[int]` means visiting every element, so a check against a 6,800-row batch would traverse 6,800 objects behind a call that looks like a type test. Nested types make it worse: `dict[str, list[tuple[str, int]]]` would recurse through the whole structure. **It is sometimes impossible.** For containers that produce values on demand there is nothing to inspect without consuming the source, which the check has no right to do. **And it would still not be a type check.** Nothing stops a caller appending a `str` to a list that passed a moment ago. Static typing reasons about how a value is used across the program; a runtime snapshot cannot. ### What does work at runtime The container class alone is checkable, and so are several nearby things people conflate with the parameterised case: * `isinstance(x, list)` — fine, the plain class. * `isinstance(x, collections.abc.Mapping)` — fine. Unparameterised ABCs answer through a subclass hook, which is how a `ChainMap` or a `MappingProxyType` reports as a `Mapping`. * `isinstance(x, collections.abc.Sequence[str])` — `TypeError`, exactly like `list[int]`; parameterising is what breaks it, not the module it came from. * `isinstance(x, int | str)` — fine since **Python 3.10**, because a union of plain classes is a set of classes with nothing to walk. So the practical pattern is to check the shape and then validate the contents yourself when you actually need the guarantee: ```python def as_counts(value: object) -> list[int]: if not isinstance(value, list): raise TypeError("expected a list") if not all(isinstance(item, int) for item in value): raise TypeError("expected a list of ints") return value ``` That is explicit about its cost — the `all(...)` really does touch every element — which is precisely the honesty `isinstance` declines to fake. ### The bigger point: annotations are never enforced The `TypeError` is a specific symptom of a general fact. CPython does not check annotations at all. A function declared `-> dict[str, int]` may return a list; a parameter annotated `int` may receive a str. Even the generic alias itself is happy to be used as a constructor and checks nothing: ```pycon >>> list[int]([1, "two"]) [1, 'two'] ``` The alias forwards the call to its origin class and hands back a perfectly ordinary list. On **Python 3.14** the annotations are not even evaluated at definition time: PEP 649/749 made them lazy, so `def pick(rows: list[NotDefinedYet]) -> None: pass` defines without error and the `NameError` surfaces only when something reads the annotations. Verification is the job of a static checker before the code runs, or of an explicit validation step written into the code — a hand-rolled guard, a dataclass with your own validation step, or a runtime data-validation library, all of which pay the traversal cost openly. ### Answering it well in an interview Name the object (`types.GenericAlias`), name the error, and give the reason in terms of what the check would have to do: walk the container, get the empty case wrong, and still not mean what static typing means. Then show the fallback — class test plus explicit element validation — and the contrast cases that *do* work, `isinstance(x, list)` and `isinstance(x, int | str)`. Candidates who instead say "type hints are checked at runtime" reveal the misconception the question exists to find.
- Does isinstance work with collections.abc.Mapping, and why?Yes, as long as it is unparameterised. The abstract base classes answer `isinstance` through a subclass hook, so a `ChainMap`, a `types.MappingProxyType` or a class that registers itself all report as a `Mapping` without inheriting from dict. Parameterise it — `Mapping[str, int]` — and you get the same `TypeError` as `list[int]`, because the object handed to `isinstance` is a generic alias again.
- Why does `isinstance(x, int | str)` work when `isinstance(x, list[int])` does not?A union of plain classes is just a set of classes, so the test is the same cheap class check repeated; CPython has accepted it since Python 3.10. A parameterised generic instead asks about the values inside a container, which means traversal, gives the wrong answer for an empty container, and cannot be answered for a lazily produced one.
- What does calling `list[int](...)` actually do?It constructs a plain list. The generic alias forwards the call to its origin class and performs no checking whatsoever, so `list[int]([1, "two"])` returns `[1, 'two']`. It is a useful demonstration that the parameters are metadata for tools rather than a runtime constraint.
Asking isinstance about list[int] is like asking whether a crate is a crate-of-apples: you can see it is a crate at a glance, but the contents question means opening it and counting, and an empty crate answers yes to every fruit.
saying these in an interview costs you the question
- Says Python checks annotations when the code runs
- Thinks isinstance(x, list[int]) inspects the elements
- Believes the TypeError is a CPython bug or oversight
- Claims isinstance never works with collections.abc classes
- Assumes list[int](...) validates what it is given
- Confuses a static checker's guarantees with runtime ones