TypeVar Defaults with PEP 696
TL;DR — PEP 696, new in Python 3.13, lets a type parameter carry a default: T = TypeVar("T", default=int), or in PEP 695 syntax class Box[T = int]. When a caller leaves the parameter off, the checker substitutes the default instead of falling back to Unknown/Any. Defaulted parameters must come after non-defaulted ones, and typing_extensions backports the feature to older runtimes.
Before PEP 696, writing Box without a type argument left the parameter unresolved — pyright reported Unknown, mypy silently used Any, and you lost type safety exactly where an ergonomic default would have helped. Defaults close that gap: a generic can declare a sensible fallback so the bare, unparameterized form is still precisely typed. This page, part of Advanced Typing Patterns & Generics and the Generics and TypeVar cluster, covers the syntax, the ordering rules, and analyzer support.
The syntax, both spellings
There are two equivalent ways to declare a default. The classic TypeVar object takes a default= keyword; the PEP 695 inline form uses = Type after the parameter name.
# Python 3.13+, checked with pyright 1.1.370
from typing import TypeVar, Generic
T = TypeVar("T", default=int)
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
reveal_type(Box(3).value) # int
reveal_type(Box("x").value) # str — explicit argument still wins
empty: Box = Box(3)
reveal_type(empty.value) # int — default fills the bare Box
# Python 3.13+, checked with pyright 1.1.370 — PEP 695 inline form
class Cache[K = str, V = int]:
def __init__(self) -> None:
self._data: dict[K, V] = {}
reveal_type(Cache()) # Cache[str, int] — both defaults apply
reveal_type(Cache[bytes, float]()) # Cache[bytes, float] — explicit wins
An explicit type argument always overrides the default; the default only supplies the parameter when the caller omits it. This is why Box (bare) is now Box[int] rather than Box[Unknown].
Ordering rules
Defaults follow the same rule as function parameter defaults: once a parameter has a default, every parameter to its right must have one too. A defaulted parameter cannot precede a non-defaulted one, because the checker could not tell which argument you meant to omit.
# Python 3.13+, checked with pyright 1.1.370
class Ok[T, U = int]: ... # fine — default is last
class Bad[T = int, U]: ...
# pyright: reportGeneralTypeIssues — non-default type parameter follows a default
# mypy: [misc] — TypeVar with a default cannot precede one without
A default may also reference an earlier parameter: class Pair[T, U = T] makes U fall back to whatever T resolved to. The referenced parameter must appear earlier in the list, mirroring the left-to-right ordering constraint.
Box(3) constructs the same object whether or not T has a default, and Python performs no substitution — the default exists purely to guide the static checker when a type argument is omitted. You can read the declared default reflectively via T.__default__ and test for one with T.has_default(), but neither changes how the code executes.
Analyzer support and the backport
Pyright implemented PEP 696 ahead of the 3.13 release and handles both spellings, defaults referencing earlier parameters, and the ordering diagnostics. mypy added support over its 1.x line; confirm you are on a recent release (1.12+) before relying on defaults, as earlier versions rejected the default= keyword outright. For runtimes older than 3.13, import the parameter constructors from typing_extensions, which backports default= to TypeVar, ParamSpec, and TypeVarTuple alike.
# Python 3.11 / 3.12, checked with pyright 1.1.370 — backport
from typing_extensions import TypeVar
T = TypeVar("T", default="ServiceConfig") # forward ref default, pre-3.13 runtime
class Loader(Generic[T]): # type: ignore[name-defined] # Generic from typing
...
Defaults work for ParamSpec and TypeVarTuple too — a ParamSpec defaults to a full parameter list (e.g. P = ParamSpec("P", default=...)), and a TypeVarTuple defaults to a tuple of types — so the mechanism spans every kind of type parameter, not just plain TypeVars. See pyright vs mypy for the current support delta.
Common mistakes
- Default before a non-default parameter:
class Bad[T = int, U]is rejected — pyrightreportGeneralTypeIssues, mypy[misc]. Move every defaulted parameter to the end of the list. - Referencing a later parameter in a default:
class Pair[T = U, U = int]fails becauseUis not yet in scope; a default may only reference parameters declared before it. - Assuming old checkers understand
default=: on mypy before 1.12 or pyright before its PEP 696 release,TypeVar("T", default=int)is flagged as an unexpected keyword. Pin a recent analyzer version. - Mixing a bound and an incompatible default:
TypeVar("T", bound=str, default=int)is contradictory — the default must satisfy the bound, or the checker reports[type-var]/reportGeneralTypeIssues.
FAQ
Do TypeVar defaults remove the need to write explicit type arguments?
Only for the fallback case. When you do supply an argument it always wins; the default merely means the bare, unparameterized generic is precisely typed instead of Unknown/Any. It is an ergonomics feature, not a replacement for parameterization where you know the type.
Can ParamSpec and TypeVarTuple have defaults too?
Yes. PEP 696 applies uniformly: a ParamSpec can default to a parameter list and a TypeVarTuple to a tuple of types, both via the same default= keyword (or PEP 695 = ... form). The ordering rule — defaults last — holds across all three kinds.