What object does Python's `type Point[T] = tuple[T, T]` statement create at runtime?
answer
- A soft keyword, not an assignment
- Produces a dedicated typing object
- The right-hand side waits
- Lets an alias name itself
- Computed once, then cached
basics
~20 sIt creates a typing.TypeAliasType object named Point, not a plain name binding. The right-hand side is stored unevaluated and computed the first time you read its value attribute, so an alias may reference names defined later.
solid answer
~40 sThe `type` soft keyword, added in Python 3.12 by PEP 695, builds a `typing.TypeAliasType` instance. It carries `__name__`, the declared `__type_params__`, and `__value__` — and `__value__` is **lazy**: the expression on the right is compiled into a hidden function and evaluated only on first access, then cached. That laziness is the point. It lets an alias refer to a name defined later in the module and lets an alias refer to itself, so `type Node = list[Node]` is legal with no string quotes. A parameterized alias can be subscripted, and `Point[float]` yields a `types.GenericAlias` whose `__origin__` is the alias object. Because a `TypeAliasType` is not a class, it cannot be used as the second argument to `isinstance`; that raises `TypeError`.
code
python · 7 linestype Reading[T] = dict[str, T | None]
print(type(Reading).__name__)
print(Reading.__name__, Reading.__type_params__)
print(Reading.__value__)
print(Reading[float])
print(Reading[float].__origin__ is Reading)go deeper
Know that type Point = tuple[float, float] is the modern way to name a type, that type here is a keyword rather than the builtin function, and that the alias is for annotations — it is not a class you can instantiate or test with isinstance.
Explain that the statement builds a typing.TypeAliasType with __name__, __type_params__ and a lazily evaluated __value__, and why that laziness is what makes recursive and forward-referencing aliases legal without quoting them as strings.
Show you know the evaluation model precisely: nothing runs at definition, the first __value__ read runs the body, and the result is cached. Be able to say when a plain assignment is still fine and why an alias never gives you a runtime type.
Own how aliases shape a codebase's vocabulary — one named shape per domain concept, defined once and widened in one place — and set the line between an alias that documents a shape and a real class that carries behaviour and identity.
## A statement, not an assignment `type Point[T] = tuple[T, T]` looks like an assignment but is a distinct statement introduced in Python 3.12 (PEP 695). `type` here is a **soft keyword**: it is only special in this position, so the builtin `type` remains callable everywhere else and existing code named `type` keeps working. The statement binds `Point` to an instance of `typing.TypeAliasType`: ```python type Point[T] = tuple[T, T] print(type(Point)) # <class 'typing.TypeAliasType'> print(Point.__name__) # Point print(Point.__type_params__) # (T,) print(Point.__value__) # tuple[T, T] ``` Contrast that with the old style, `Point = tuple[float, float]`. A plain assignment binds an ordinary object — here a `types.GenericAlias` — and a checker has to *guess* from context that you meant an alias rather than a value. The `type` statement says so explicitly, and produces a distinct runtime object that tools can recognise. ## Laziness is the feature The expression on the right-hand side is not evaluated when the statement runs. The compiler stores it as a hidden zero-argument function; the first read of `__value__` calls it and caches the result. Two consequences follow, and both are the reason the feature exists. First, **forward and recursive references work without quotes**. A JSON-shaped alias can mention itself: ```python type Json = str | int | float | bool | None | list[Json] | dict[str, Json] ``` On a plain assignment that is a `NameError`, because `Json` is not bound yet when the right side runs. With the `type` statement the body runs later, by which time `Json` is bound to the alias object, so the self-reference resolves. Second, **the cost is deferred and paid once**. A module full of aliases does no work at import time, and an alias whose value is never inspected is never evaluated at all. The result is memoised on the alias object, so an expression with a side effect in it runs exactly once no matter how many times the value is read: ```python evaluations = [] def probe(t): evaluations.append(t) return t type Row = probe(dict[str, float]) print(evaluations) # [] - nothing has run yet Row.__value__ Row.__value__ print(len(evaluations)) # 1 - evaluated once, then cached ``` That is worth knowing precisely because the intuition cuts both ways. Someone who believes the body is eager will expect an import-time failure that never comes; someone who believes it re-runs per access will fear a repeated side effect that also never comes. ## Parameterizing and using an alias An alias declared with brackets is subscriptable. `Point[float]` produces a `types.GenericAlias` whose `__origin__` is the `TypeAliasType` and whose arguments are what you supplied; a checker substitutes them into the value. The alias itself is perfectly usable in annotations, unsubscripted, when it has no parameters. What it is *not* is a class. `isinstance(x, Point)` raises `TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union`, and you cannot subclass an alias. Aliases name shapes for the type system; they do not create runtime types. Anyone who wants a genuinely distinct runtime-adjacent identity is reaching for a different tool. ## Where a lab-shaped example makes it concrete Consider a clinical-lab result loader with an alias `type Reading[T] = dict[str, T | None]`, where `None` marks an assay that did not report. Writing that shape once and referring to `Reading[float]` in a dozen signatures is exactly what aliases are for: the shape has one definition, and widening it — say, to allow a string-coded result — is one edit rather than a dozen. Because the value is lazy, the alias can sit at the top of the module and still refer to record classes defined further down. ## Version notes The `type` statement and `typing.TypeAliasType` are **3.12**. Type-parameter defaults on an alias (`type Table[K = str, V = float] = dict[K, V]`) are **3.13**. In **3.14**, annotations themselves became lazily evaluated by default under PEP 649, with `annotationlib` for inspecting them — which means the alias's laziness and the annotation system's laziness now line up rather than being two different models a reader has to hold at once. The older workaround for forward references, `from __future__ import annotations`, was never needed for the `type` statement in the first place: aliases were lazy from the day the statement shipped.
- Is the right-hand side of a `type` statement re-evaluated on every access to `__value__`?No. It is evaluated on the first access and the result is cached on the alias object, so a side-effecting expression in the body runs exactly once, however many times the value is read. Before that first read it has not run at all, which is why an alias can mention a name defined later in the module.
- How does `type X = int` differ from the plain assignment `X = int`?`type X = int` creates a `typing.TypeAliasType` with a lazy `__value__` and declares the intent explicitly; `X = int` just binds the name to the class, and a checker infers alias-hood from context. The statement form also supports parameters and self-reference. The plain binding stays usable at runtime — `isinstance(3, X)` works there but raises `TypeError` on the alias.
- Can `type` still be used as a variable name after this statement existed?Yes. `type` is a soft keyword: it is only special at the start of a type-alias statement. The builtin `type()` and any local or parameter named `type` continue to work, which is what let PEP 695 add the statement without breaking existing code.
A plain assignment is a photograph of the type taken immediately; the type statement is a note saying where to look, read only when someone asks and remembered afterwards.
saying these in an interview costs you the question
- Reads `type X = ...` as calling the builtin type()
- Says the right-hand side is evaluated at the statement
- Thinks re-reading __value__ re-runs the body each time
- Uses an alias as the second argument to isinstance
- Believes recursive aliases still need string quotes
- Claims the statement produces no runtime object at all