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.

Omitted type parameter with and without a default Using Box without a type argument yields Unknown or Any when no default is set, but resolves to the declared default type int when PEP 696 default is present. Box (used bare, no argument) no default class Box[T] T resolves to Unknown / Any safety lost PEP 696 default class Box[T = int] T resolves to int precise fallback
A PEP 696 default gives an unparameterized generic a precise fallback instead of Unknown or Any.

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.

Runtime vs static analysis A PEP 696 default is inert at runtime. 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 — pyright reportGeneralTypeIssues, 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 because U is 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.

Back to Generics and TypeVar