type parameter bound ranges¶
a type parameter bound is written T: Upper, which pins only the top of the range — T may still
be specialized to anything below Upper, including Never. a bound range pins both ends:
the left end is the lower bound and the right end is the upper bound, so this reads
str <: T <: object. both ends are required; T: ..object is spelled T: object, and T: str..
has no spelling because an upper end is always needed to bound the range. the two dots are written
as one unit — T: str . . object is not a range.
what the lower end buys you¶
with only an upper bound, nothing is assignable to a type parameter — the checker cannot know
that a given T is wide enough to hold anything at all:
a lower bound puts a floor under every specialization, so anything at or below the lower end is
assignable to T:
this is the constraint a default implementation in a generic base class needs: the body is written against a particular type, and the bound records which specializations it is valid for.
it is a bound, not an equality¶
every type at or above the lower end is a valid specialization:
class C[T: str..object]: ...
C[str] # ok
C[object] # ok
C[str | int] # ok
C[int] # error: `int` does not satisfy lower bound `str`
the upper end is unchanged¶
T: Lower..Upper behaves exactly like T: Upper everywhere the upper bound is consulted — member
lookup, narrowing, and specialization checks all see the same upper bound they would have seen
without the range. in particular the lower end says nothing about what a T has:
class C[T: str..object]:
def f(self, x: T) -> int:
return len(x) # error: `T@C` is not `Sized` — that is the upper end's job
both ends must accept the default¶
a default is a specialization like any other:
class C[T: str..object = int]: ... # error: default `int` is not assignable from lower bound `str`
class D[T: str..str = object]: ... # error: default `object` is not assignable to upper bound `str`
Self is a valid lower end¶
Self is bound by the enclosing class, not by the generic context being declared, so it is exempt
from the rule that a bound cannot be generic — at either end:
a range needs a plain upper end¶
a type mapping is an unordered set rather than the top of a range, so the two
forms are alternatives — in and : cannot both introduce the same parameter. a parameter list is
not a type either, so it cannot cap a range:
empty ranges¶
if the lower end is not assignable to the upper end, no type can satisfy the range and the declaration is an error:
composing the ends¶
each end is an ordinary type expression, so unions and intersections compose the usual way. an intersection on the upper end narrows it; a union on the lower end widens the floor:
scope¶
ranges are a .by reading only. python's type-parameter grammar has a single bound, so .. in a
.py type parameter list is a syntax error.
lowering¶
python bounds have no lower end, so it is erased and only the upper end is emitted:
transpiles to:
or, when the Generic[...] polyfill applies, to
_T = TypeVar("_T", bound=object). the lower bound is checked against the .by source, not the
emitted python.
see also¶
- generics — the type parameter forms
- bounds on a variadic pack — what a bound means on a
*Tsor**Kwargs - type mappings —
T in (int, str), an unordered alternative to a range - typevar variance keywords — which direction subtyping moves a specialization