What does the `= int` do in Python's `class Box[T = int]` declaration?
answer
- An equals sign inside the brackets
- One release after the bracket syntax
- Ordering mirrors function arguments
- The parser enforces it, not a checker
- A sentinel distinguishes none from None
basics
~20 sIt gives the type parameter T a default, so an unsubscripted Box is read as Box[int] by a type checker. PEP 696 added defaults in Python 3.13; at runtime the parameter reports that it has one.
solid answer
~40 sPEP 696, shipped in Python 3.13, lets a type parameter carry a default: `class Box[T = int]` means a bare `Box` in an annotation is treated as `Box[int]` rather than as an unparameterized generic. It works the same way on functions and on `type` aliases — `type Table[K = str, V = float] = dict[K, V]`. The one structural rule is ordering: a parameter with no default may not follow one that has a default, and violating it is a `SyntaxError` at compile time, not a checker complaint. At runtime the defaults are introspectable on the `typing.TypeVar` objects in `__type_params__`: `has_default()` returns a boolean and `__default__` returns the default, or the `typing.NoDefault` sentinel when there is none. Nothing is substituted at runtime — the default is information for the type system.
code
python · 17 linesimport typing
class Box[T = int]:
def __init__(self, item: T) -> None:
self.item = item
(tv,) = Box.__type_params__
print(tv.has_default(), tv.__default__)
type Table[K = str, V = float] = dict[K, V]
print([p.__default__ for p in Table.__type_params__])
def g[U](x: U) -> U:
return x
(uv,) = g.__type_params__
print(uv.has_default(), uv.__default__ is typing.NoDefault)go deeper
Recognise the shape when you read it: the = int inside the brackets is a default for the type parameter, so a bare Box means Box[int]. It changes nothing at run time and it is a Python 3.13 feature.
Explain that defaults are PEP 696 in 3.13, a release after the 3.12 bracket syntax, that they apply to functions, classes and type aliases alike, and that a non-defaulted parameter following a defaulted one is a SyntaxError rather than a checker warning.
Demonstrate the introspection detail: use has_default() rather than testing __default__, because None is a valid default and typing.NoDefault is the sentinel for absence. Know the 3.13 floor a default imposes on a library's supported runtimes.
Judge when a default is a service to readers and when it hides a decision. A default makes the common case terse, but every bare use of the generic now carries an invisible choice — decide whether the codebase's vocabulary is better served by explicitness.
## The feature Python 3.13 added PEP 696, type-parameter defaults, on top of the 3.12 bracket syntax. A default is written with `=` inside the parameter list: ```python class Box[T = int]: def __init__(self, item: T) -> None: self.item = item ``` To a type checker, an annotation that says just `Box` now means `Box[int]`. Without a default, a bare `Box` is an incompletely parameterized generic, and checkers treat that as implicitly `Box[Any]` or flag it, depending on strictness settings. A default turns "unspecified" into a deliberate, documented choice. The same syntax works in all three places the bracket list appears: ```python def collect[T = str](values: list[T]) -> set[T]: ... class Box[T = int]: ... type Table[K = str, V = float] = dict[K, V] ``` ## Where it earns its keep The motivating case is a generic that is *usually* used at one type. In a clinical-lab result loader, a container of assay results is nearly always keyed by assay code and valued by a float measurement; the rare case carries a string-coded result instead. Writing `type Panel[V = float] = dict[str, V | None]` lets most of the codebase say `Panel` and the outlier say `Panel[str]`, instead of forcing every one of a hundred annotations to repeat the common case. It is a readability feature, and its cost is that a reader must know the default exists — which is why keeping the default obvious and boring matters more than saving keystrokes. ## The ordering rule One constraint applies: **a parameter without a default may not follow one with a default**, exactly as for function arguments. Unlike most typing mistakes, this one is caught by the parser: ```python try: exec("class Box[T = int, U]: ...") except SyntaxError as exc: print(exc) # non-default type parameter 'U' follows default type parameter ``` That it is a `SyntaxError` rather than a checker diagnostic is the memorable detail: the file will not import, with or without a type checker in the loop. ## Runtime introspection Defaults are visible on the `typing.TypeVar` objects the brackets create: ```python import typing class Box[T = int]: ... (tv,) = Box.__type_params__ print(tv.has_default()) # True print(tv.__default__) # <class 'int'> def g[U](x: U) -> U: ... (uv,) = g.__type_params__ print(uv.has_default()) # False print(uv.__default__ is typing.NoDefault) # True ``` `has_default()` and the `typing.NoDefault` sentinel are both 3.13 additions. The sentinel exists because `None` is itself a legitimate default — a parameter really can default to `None` — so a falsy return could not distinguish "defaults to None" from "has no default". Prefer the `has_default()` predicate in introspection code; it says what you mean. As with everything else in the bracket syntax, nothing happens at call time. Constructing `Box(3.5)` does not check or coerce anything, and the default does not make the instance "a `Box[int]`" in any runtime sense. The default is a statement to the type system, plus a runtime-inspectable record of that statement. ## The pre-bracket spelling The same capability landed on the old API in 3.13: `TypeVar("T", default=int)`. That matters for libraries that still support Python 3.12 or earlier runtimes and therefore cannot use the bracket syntax at all — though on 3.12 the `default=` keyword itself is unavailable, so a truly wide-support library reaches for the backported typing extensions distribution instead of the stdlib. On a codebase whose floor is already 3.13, the bracket form is the one to write. ## Versions, stated exactly Bracket type parameters: **3.12**. Type-parameter defaults, `has_default()` and `typing.NoDefault`: **3.13**. Nothing in this area changed in **3.14**, whose typing news was elsewhere — deferred annotation evaluation under PEP 649 and the `annotationlib` module. If asked to date the feature, the trap is answering "3.12, with the rest of PEP 695"; defaults are a separate PEP that shipped one release later.
- May a type parameter without a default follow one that has a default?No — the rule mirrors function arguments, and Python enforces it in the parser. `class Box[T = int, U]` raises `SyntaxError: non-default type parameter 'U' follows default type parameter`, so the module does not import at all. It is one of the few typing mistakes a type checker never gets the chance to report.
- How do you tell at runtime whether a type parameter has a default?Call `has_default()` on the `typing.TypeVar` from `__type_params__`. Reading `__default__` alone is ambiguous, because `None` is a legitimate default value; when there is no default, `__default__` returns the `typing.NoDefault` sentinel rather than `None`. Both the predicate and the sentinel arrived in 3.13 with PEP 696.
- What is the equivalent without the bracket syntax?`T = TypeVar("T", default=int)`, also added in 3.13. It is what a library still supporting Python 3.12 or earlier runtimes would reach for — though on those interpreters the `default=` keyword is not in the stdlib either, so wide-support code uses the backported typing extensions distribution instead.
saying these in an interview costs you the question
- Dates defaults to 3.12 alongside the rest of PEP 695
- Thinks the default is applied or coerced at runtime
- Says any ordering of defaulted parameters is allowed
- Confuses a type-parameter default with a default argument value
- Reads __default__ of None as meaning no default
- Expects a type checker rather than the parser to flag bad ordering