Skip to content
← Back to explainers

Python explainer

Give a Type Parameter a Default Type

About 3 min to read. Allow extra time to work through examples.

Give a type parameter a default, so that a bare Repo means Repo[str]. Use Python 3.13's syntax, or typing_extensions on Python 3.12.

This article uses Python. You can make it your practice mode to browse related content across the site.

Say you write a small generic store, Repo[T], and almost every caller stores strings. Under mypy --strict, each caller must still write Repo[str], because a bare Repo is an error:

class Repo[T]:
    def __init__(self, value: T | None = None) -> None:
        self.value = value

def load(repo: Repo) -> None:  # error: Missing type arguments for generic type "Repo"
    ...

Python 3.13 lets you give the type parameter a default type. mypy uses the default when the caller leaves the type out and there is nothing to infer it from.

Write the default after an equals sign

This example needs Python 3.13 or newer:

from typing import assert_type

class Repo[T = str]:
    def __init__(self, value: T | None = None) -> None:
        self.value = value

def load(repo: Repo) -> None:  # fine: a bare Repo means Repo[str]
    ...

assert_type(Repo(), Repo[str])
assert_type(Repo(7), Repo[int])
assert_type(Repo[int](), Repo[int])

assert_type asks mypy to confirm a type, and does nothing at runtime. Repo() passes nothing to infer T from, so mypy uses the default, str. Repo(7) passes an int, so mypy infers int. An explicit Repo[int] takes priority over the default and over inference. The default is only a type for mypy, so Repo().value is still None.

Functions and type aliases can have defaults too:

from typing import assert_type

type Pair[T = int] = tuple[T, T]

def identity[T = bytes](value: T) -> T:
    return value

pair: Pair = (1, 2)  # Pair means tuple[int, int]
assert_type(identity("hello"), str)

mypy infers str for identity("hello") from the argument, and ignores the default.

Put the defaults last

Type parameters with defaults go after all the others, like default values for function arguments:

class Options[T, U = int, V = str]:
    pass

options: Options[bool] = Options()  # Options[bool, int, str]
both: Options[bool, float]          # Options[bool, float, str]

class Bad[T = int, U]: ... is a SyntaxError.

Check your understanding Why must type parameters with defaults come after those without one? Show answer

Type arguments fill the parameters from left to right. In Bad[T = int, U], the first argument you write always goes to T. To give U a type, you must also write a type for T, so mypy never uses the default for T. With the defaults last, you leave arguments off the end, and the defaults fill the gaps.

Support Python 3.12 with typing_extensions

The [T = str] syntax is a SyntaxError on Python 3.12. To support 3.12, use the older TypeVar spelling from the article "Generic Functions from Zero". Import TypeVar from the typing_extensions package, which you install with pip:

from typing import Generic, assert_type
from typing_extensions import TypeVar

Value = TypeVar("Value", default=str)

class LegacyRepo(Generic[Value]):
    def __init__(self, value: Value | None = None) -> None:
        self.value = value

assert_type(LegacyRepo(), LegacyRepo[str])
assert_type(LegacyRepo(7), LegacyRepo[int])

The TypeVar in typing accepts default only from Python 3.13. On 3.12 it raises TypeError.

Check your understanding What does class TextRepo(Repo) inherit when Repo has the default str? Show answer

It inherits from Repo[str], because a bare Repo means Repo[str]. So TextRepo().value has the type str | None. Without the default, mypy --strict rejects the bare Repo in the class line, with the same missing-type-arguments error as before.

Pick a default only when it has one clear meaning

Add a default when leaving the type out has one clear meaning, as str does for Repo. Keep the parameter required when callers should choose. Avoid Any as a default, because it turns off checking for every caller who leaves the type out. Adding a default to a class that people already use changes what a bare annotation and a bare base class mean. Check those uses before you release the change.

Going further

  • Python 3.13 can also report a default at runtime. T.has_default() says whether a type parameter has one, and T.__default__ holds it, or typing.NoDefault when there is none.
  • A TypeVarTuple such as *Ts takes any number of types, and its own rules apply. A type parameter right after *Ts cannot have a default.
  • Read PEP 696 and the typing specification for the rules on bounds, constraints and TypeVarTuple.

step 25 of 26

Precise Type Hints with Literals and Generics

Up next Read Annotations at Runtime in Python 3.14