Bounded vs Constrained TypeVars
TypeVar("T", bound=Number) sets an upper bound — T can be any subtype of Number, and you
may use every method Number defines. TypeVar("T", int, str) sets constraints — T must be
exactly one of the listed types at each call site, and the body may only use operations common to
all of them. Under PEP 695 these become [T: Number] and [T: (int, str)]. Misuse surfaces as
[type-var] in mypy.
Both forms restrict what a TypeVar may bind to, but they restrict in opposite ways and produce different inference. A bound is a ceiling that still admits the whole subtype tree below it; constraints are an enumeration that admits only the exact listed types. Choosing the wrong one either over-restricts your API or loses precise return types. This guide, part of Advanced Typing Patterns & Generics, covers the semantics, inference differences, and the modern PEP 695 spelling for both.
Context: two PEP 484 mechanisms
Both forms come from PEP 484. A bounded TypeVar declares an upper bound with bound=; the
variable can bind to that type or any subtype, and inside a generic function you may call any method
the bound type guarantees. A constrained TypeVar lists two or more types positionally; the
variable must resolve to exactly one of them.
# Python 3.8+, mypy 1.x / pyright 1.1.x — legacy spelling
from typing import TypeVar
from numbers import Number
TBound = TypeVar("TBound", bound=Number) # any subtype of Number
TConstr = TypeVar("TConstr", int, str) # exactly int OR exactly str
Step 1: a bounded TypeVar preserves the concrete subtype
With bound=, the checker infers the most precise type at the call site and lets you use the
bound’s interface in the body.
# Python 3.11+, mypy 1.x / pyright 1.1.x
from typing import TypeVar
class Money: ...
class USD(Money): ...
TMoney = TypeVar("TMoney", bound=Money)
def tag(value: TMoney) -> TMoney:
return value
reveal_type(tag(USD())) # USD — the concrete subtype is preserved, not Money
Because TMoney is bounded, tag(USD()) returns USD, not Money. The body may use any attribute
declared on Money.
Step 2: a constrained TypeVar collapses to a listed type
Constraints behave differently: the inferred type is always one of the listed types, never a subtype and never a union.
# Python 3.11+, mypy 1.x / pyright 1.1.x
from typing import TypeVar
TConstr = TypeVar("TConstr", int, str)
def doubled(value: TConstr) -> TConstr:
return value + value # only operations valid for BOTH int and str
reveal_type(doubled(3)) # int
reveal_type(doubled("ab")) # str
reveal_type(doubled(True)) # int — bool is narrowed UP to the listed int
doubled(True) returns int, not bool, because bool is not in the list and the checker selects
the listed type bool is compatible with. The body may use only operations valid for every listed
type — here + works for both int and str.
Step 3: mixing types is what each rejects
A bound rejects anything outside the subtype tree. Constraints reject anything that is not exactly one of the listed types, and reject mixing them in one call.
# Python 3.11+, mypy 1.x
from typing import TypeVar
TConstr = TypeVar("TConstr", int, str)
def first(a: TConstr, b: TConstr) -> TConstr:
return a
first(1, 2) # ok -> int
first("a", "b") # ok -> str
first(1, "b") # mypy error: [type-var] — int and str can't satisfy one TypeVar together
Step 4: PEP 695 equivalents
Python 3.12’s PEP 695 type parameter syntax
expresses both inline. A bound is [T: Bound]; constraints are [T: (A, B)].
# Python 3.12+, mypy 1.x / pyright 1.1.x — PEP 695 inline form
def tag[TMoney: Money](value: TMoney) -> TMoney: # bound
return value
def doubled[TConstr: (int, str)](value: TConstr) -> TConstr: # constraints
return value + value
The parenthesised tuple (int, str) is what marks constraints; a bare [T: Money] is a bound.
This is the recommended modern spelling on 3.12+; the TypeVar(...) form remains for older runtimes.
Edge cases
boolunder constraints: WithTypeVar("T", int, str), aboolargument binds toint(its nearest listed supertype), so the return type isint, surprising callers who expectedbool.- A single constraint is illegal:
TypeVar("T", int)with one type is rejected — constraints need at least two. Usebound=intif you meant “int or a subtype”. - Constraints don’t union:
doublednever returnsint | str; it returns whichever single listed type matched. If you want a union return, use an explicitint | strannotation, not a constrainedTypeVar.
Common mistakes
- Using constraints when you meant a bound:
TypeVar("T", int, float)forbids subtypes and forbids mixing; if you wanted “any number”, usebound=. Wrong choice often shows as[type-var]at call sites. - Expecting the body to use subtype-specific methods under constraints: The body may use only
operations common to all listed types; calling a
str-only method triggers[union-attr]/[attr-defined]. - Combining
bound=and constraints: RaisesTypeErrorat import time. Choose exactly one.
FAQ
When should I prefer a bound over constraints?
Use a bound when you want “this type or anything below it” and need the bound’s methods — the common
case for numeric or protocol-based generics. Use constraints only for a closed set of unrelated
exact types, such as str vs bytes.
Why does my constrained TypeVar return int for a bool input?
bool is a subclass of int but is not itself a listed constraint, so the checker resolves the
variable to the listed supertype int. Constraints never preserve subtypes.