A value has a type whether or not you write it down, and Python reports a mistake about it when the operation runs. Static types let a tool report that mistake before the program runs.
C++ and Java require type declarations, and they check those types during compilation. The Python runtime checks types only when an operation runs. The examples so far have used few type declarations. On a small program you may not miss them.
Python 3.0 let you annotate
parameters and return values (PEP 3107) but left the meaning of
those annotations to third-party libraries. Python 3.5 (2015)
introduced type hints (PEP 484), which look like the
type declarations of statically typed languages. The Python
runtime never acts on a type hint. It stores the hint and
evaluates it only when code reads the annotations, through
__annotations__ or
annotationlib.get_annotations().
Older code often begins with a __future__ import
of annotations (PEP 563), an earlier form of
postponed evaluation that stores each annotation as a string.
Deferred evaluation, the default since Python 3.14 (PEP 649),
supersedes it, so the book’s listings leave that import out.
If you want static type checking
like you get from a compiler in a typed language, you must run a
separate type-checking tool. Mypy is the original and most
widely deployed one. Pyright is the one most editors run. This
book uses Astral’s
ty, from the same group that makes
uv and ruff.
Pyright checks the same listings as a second opinion. The
book’s build relies on ty’s verdict alone. Where
Pyright or mypy disagrees with ty on a listing, the
text says so and names the checker, because a verdict on a hard
case is a fact about one checker, not about Python.
You can add type hints one function
at a time. Code without hints still works. The type checker
treats each missing annotation as the type Any,
which is compatible with everything. Thus, typed and untyped
code can coexist, and that coexistence is gradual
typing. You can slowly add hints where they help most: the
public interfaces, the tricky data, the code on which other
people depend.
An Any you write marks
a value as dynamic. ty reports the Any
that comes from a missing annotation as Unknown, to
distinguish it from an Any you wrote yourself. The
two behave the same. Each is compatible with everything.
Any and object differ, though both
accept every value. object is the base of every
class, so an object value carries the methods every
class shares. The type checker rejects every operation beyond
those methods. Any permits every operation, so it
switches the checker off for that value rather than describing a
wide set of values. Choose object for a value you
store or pass along untouched, and Any for one
whose operations you would rather check yourself.
A hint annotates a parameter, a return value, or a variable. Use a colon for parameters and variables, and an arrow for the return type:
# typed_basics.py
def repeat(text: str, times: int) -> str:
return text * times
print(repeat("ab", 3))
#: ababab
total: int = 0
for word in ["a", "bb", "ccc"]:
total += len(word)
print(total)
#: 6Containers and optional types read the way you say them:
list[int], dict[str, float],
tuple[int, ...], and str | None for “a
string or nothing.” A tuple annotation specifies one type per
position, so tuple[int, str] is a pair and
tuple[int] is a tuple of one int. The
... in tuple[int, ...] means zero or
more ints.
A function declares -> None when it returns
None, by a return with no value or by
reaching its end, so every __init__() in this
chapter’s listings carries that annotation.
tyPython runs the code the same with or without the hints. You need a tool to check them:
ty check
uvx ty check runs it
in a temporary, isolated environment, and
uv tool install ty@latest puts it on your path.
ty reports an error where the hints and the code
disagree, and stays silent when they agree. The build runs
ty on every change, so the code you read here
checks as well as runs.
Type checking discovers mistakes before the program runs.
# area.py
def area(width: int, height: int) -> int:
return width * height
# ty: argument of type "str" is not assignable to "int":
print(area("3", 4)) # type: ignore
#: 3333At runtime area("3", 4) succeeds. It returns
"3333", because "3" * 4 repeats the
string four times. The wrong value causes a failure later, often
far from the line that produced it. The type checker reports the
str argument at the call, before the program
runs.
The # type: ignore comment tells the type
checker to skip this line, so this book’s build passes. Without
it, ty check reports:
error[invalid-argument-type]: Argument to function `area` is incorrect
--> area.py:6:12
|
6 | print(area("3", 4))
| ^^^ Expected `int`, found `Literal["3"]`
info: Function defined here
--> area.py:2:5
|
2 | def area(width: int, height: int) -> int:
| ^^^^ ---------- Parameter declared here
A diagnostic names the rule in brackets, points at the offending line, pairs what the annotation expected with what the call supplied, and then points at the declaration that set the expectation.
Listings in this book use a
shorthand for a diagnostic. A neighboring # ty:
comment summarizes what the type checker reports for a line. For
a line that fails the check, commented out or suppressed with
# type: ignore, the comment gives the diagnostic.
For a reveal_type() call, the comment gives the
type the call produces. reveal_type(), imported
from typing, marks an expression whose inferred
type you want to see, and ty check prints that type
as a diagnostic.
The # ty: summary is for you, not for the tool.
ty acts on three comment forms.
# type: ignore and a bare # ty: ignore
silence every diagnostic on their line, and
# ty: ignore[invalid-argument-type] silences the
one rule it names. # type: ignore is the form every
type checker honors.
A union type covers every case until you rule some out.
Testing is not None on an X | None
value proves to the type checker, as well as to you, that the
value is an X:
# narrowing.py
def shout(text: str | None) -> str:
if text is not None:
return text.upper()
return "(nothing)"
print(shout("hi"))
#: HI
print(shout(None))
#: (nothing)Inside the if, the
type checker narrows text from
str | None to str, so the type checker
accepts .upper() as written. After the
if, text is None, since
the branch returned for every str. If the branch
falls through, text is the full
str | None again.
The same narrowing follows an
isinstance() check, an equality test, or an
identity test against a specific value such as
is not SOME_SENTINEL. A truthiness test,
if text:, also narrows text to
str, since None is falsy. But the
empty string is falsy too, so if text: sends
"" to the other branch along with
None. Test is not None when an empty
value counts as supplied (see exercise 8).
Narrowing also makes an
object parameter usable. Gradual Typing says the type checker
rejects every operation on an object beyond the
methods every class shares, so a function needs an
isinstance() test before it can do more with the
value:
# object_narrowing.py
def describe(value: object) -> str:
# ty: "object" has no attribute "upper":
# return value.upper()
if isinstance(value, str):
return value.upper()
return repr(value) # [1]
print(describe("hi"))
#: HI
print(describe(42))
#: 42repr() accepts every object, so the
return at [1] handles any value that
reaches it. If you annotate value as
Any and uncomment the return above the
if, the type checker accepts it, and
describe(42) fails at runtime with an
AttributeError.
Narrowing an attribute is riskier than narrowing a local variable. A call between the check and the use can reset that attribute. The type checker assumes the call leaves the attribute as it was:
# narrowing_attribute.py
from exceptions import expect
class Box:
def __init__(self, val: str | None) -> None:
self.val = val
def reset(self) -> None:
self.val = None
def show(b: Box) -> str:
if b.val is not None:
b.reset() # ty can't see this clears val
return b.val.upper()
return "(nothing)"
expect(AttributeError, show, Box("hi"))
#: [AttributeError] 'NoneType' object has no attribute
#: 'upper'ty check passes this file. The if
narrows b.val to str, and the checker
keeps that narrowing through the return, because
the checker does not track which attributes reset()
assigns. The AttributeError shows that
b.val was None by the time
upper() ran, while the checker still treated it as
a str.
A narrowing on a local variable holds. A narrowing on an attribute holds until a call rebinds the attribute, so recheck the attribute after any call that might change the object. Copying the attribute into a local variable before the test keeps the narrowing valid, because a function defined elsewhere cannot rebind its caller’s local variable, short of stack-frame introspection (see exercise 9).
Final on a name makes
the type checker report an accidental reassignment.
The naming convention in Tour
uses ALL_CAPS to signal a constant, but that is only a hint to
human readers. At runtime, a Final name is an
ordinary variable. Only the type checker rejects a
reassignment:
# final_constants.py
from typing import Final
MAX_RETRIES: Final = 3
GREETING: Final[str] = "hello"
HISTORY: Final[list[str]] = []
# ty: cannot assign to final name "MAX_RETRIES":
# MAX_RETRIES = 5
HISTORY.append("first")
print(MAX_RETRIES, GREETING, HISTORY)
#: 3 hello ['first']The word Final
suggests immutability, but the qualifier blocks rebinding the
name, not mutation of the object the name holds.
HISTORY.append("first") checks and runs, the same
as it does on any list. For a collection that must stay as it
is, hold a tuple or a frozenset under the
Final name, since neither type has a method that
changes its contents (see exercise 10).
You can write the type in the
annotation, as in GREETING, or let the type checker
infer it from the value, as with MAX_RETRIES. The
two forms differ when the initializer says less than you mean.
CACHE: Final = [] infers
list[Unknown], so the type checker accepts any
value you put into the list.
CACHE: Final[list[str]] = [] says what the list
holds, so the type checker enforces list[str].
Every other listing in this book writes the type out on
Final.
Earlier chapters rely on dynamic typing. A function accepts any object, so long as the object supports the operations the function performs on it. Python checks the type at runtime, when the operation runs. Programmers often call dynamic typing duck typing. If it looks like a duck and quacks like a duck, treat it as a duck.
Structural typing is the static counterpart. A type checker verifies ahead of time that an object has the required shape. “Shape” means the methods and attributes that the code using the object requires. Dynamic typing and structural typing are the same idea checked at different moments. Dynamic typing finds a missing method when the call runs, while structural typing reports it before the program runs.
Some statically typed languages make you declare up front
that a class “is a” Drawable by inheriting from it.
A Protocol describes a required shape. Any object with
that shape qualifies, whatever its base classes:
# protocols.py
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "circle"
class Square:
def draw(self) -> str:
return "square"
def render(shape: Drawable) -> str:
return shape.draw()
class Blob:
def paint(self) -> str:
return "blob"
print(render(Circle()))
#: circle
print(render(Square()))
#: square
# ty: expected "Drawable", found "Blob":
# render(Blob())Neither Circle nor Square mentions
Drawable. The type checker accepts both because
each has a draw(self) -> str, so each matches
Drawable’s shape. The signature is part of that
shape. A draw() that returns an int,
or that requires an argument, gives its class a different shape.
The type checker rejects that class where an annotation
specifies Drawable.
Blob draws, in the everyday sense, but its
method’s name is paint(). A protocol matches on
names and signatures rather than on intent. A protocol accepts
any class with the right methods, as dynamic typing does, and
reports a mismatch before the program runs, as static type
checking does.
A Protocol is a
checking-time construct, so
isinstance(Circle(), Drawable) raises a
TypeError instead of returning True or
False. Decorating the Protocol with
@runtime_checkable allows the call, but
isinstance() then makes a weaker
check. It confirms that each method exists and ignores its
signature.
type[C]A class is also a value, so you can pass it to a function, store it in a variable, and call it to make an instance. An annotation needs a way to distinguish the class from an instance of that class.
A plain SomeType annotation means an instance of
SomeType. The form type[SomeType]
means the class object, or any subclass of it:
# class_values.py
from typing import reveal_type
class Shape:
pass
class Circle(Shape):
pass
def make(kind: type[Shape]) -> Shape:
return kind()
shape = make(Circle)
reveal_type(shape) # ty: Shape
print(type(shape).__name__)
#: CirclePassing Circle works because Circle
is a subclass of Shape.
The word type plays two roles in this listing:
the annotation type[Shape] names the class to the
type checker, while the built-in call type(shape)
in the demo retrieves an object’s class at runtime. The type
checker and the runtime give different answers for
shape. reveal_type(shape) reports
Shape, because the type checker reads
make()’s -> Shape return
annotation, while type(shape) finds the
Circle that kind() built.
A type[...] parameter accepts a class. It
rejects a type expression such as
int | None, which can appear in an annotation but
is a union rather than a class, so passing it draws
invalid-argument-type. A function that validates or
converts data against an arbitrary annotation takes a type
expression as its argument. Python 3.15 (PEP 747) adds
TypeForm[T] to annotate that parameter. Given
def convert[T](form: TypeForm[T], raw: object) -> T,
the call convert(int | None, 3) has the type
int | None.
type
StatementAn annotation can grow to the
point of obscurity. dict[tuple[int, int], str] is
precise, but it leaves the reader to guess what those pairs and
strings represent. The type statement gives the
annotation a name:
# type_aliases.py
from typing import Literal
type Coord = tuple[int, int]
type Color = Literal["red", "blue", "green", "yellow"]
type Grid = dict[Coord, Color]
def paint(grid: Grid, cell: Coord, color: Color) -> None:
grid[cell] = color
grid: Grid = {}
paint(grid, (2, 3), "red")
print(grid)
#: {(2, 3): 'red'}
print(type(grid).__name__)
#: dictLike match, type is a soft
keyword. It is a keyword only at the start of this
statement. Everywhere else, type is still the
built-in type() function, so the listing’s
type(grid) call returns dict as it
always has.
A type statement
creates a new name, not a new type. Coord and
tuple[int, int] are interchangeable, so the type
checker accepts any pair of ints as a Coord.
type UserId = int reads like a distinct type in a
signature, yet the type checker accepts any int as
a UserId. (For a type the type checker keeps
separate from its base, use NewType.)
Because an alias is the same type under another name, the
type checker sees through it. Its job is to tell the reader what
a value is. A compound shape is the usual case, but a scalar
gets an alias when its name says what a bare str or
int cannot: dict[Name, int] says the
keys are names, where dict[str, int] hides what
they are. Skip the alias when the parameter’s own name says it,
as name: str does.
Color specifies a
union of literal values instead of a union of types.
Literal["red", "blue", "green", "yellow"] restricts
the parameter to those four strings. Passing
"purple" to paint() is a type error,
even though "purple" is a valid str.
The alias also documents the allowed values in one place,
instead of scattering the literal list across every function
that accepts a Color.
A Literal union closes a set of values without
defining a class. If those values need behavior or an identity
of their own, an Enum is the better fit. Data
Classes as Types makes the case for an Enum
whenever the set of values is small and closed, then shows when
an Enum beats a data class.
An alias can also name a union of types. Pattern
Matching uses type Shape = Circle | Square to
define a closed set of alternatives that a match
can check exhaustively.
Consider a function that returns the first element of a list holding any type. A useful annotation makes the return type match the list’s element type, whatever that type is.
Any loses that connection.
first_any() accepts any list, but its return type
is Any whatever the list holds:
# first_any.py
from typing import Any
from exceptions import expected
def first_any(items: list) -> Any:
return items[0]
n = first_any([10, 20, 30])
with expected(AttributeError):
n.upper()
#: [AttributeError] 'int' object has no attribute 'upper'ty check passes this file. n is
Any, so every attribute access on it type-checks,
including one that runs and fails.
expected() is the with form of the
expect() helper that Functions introduces.
The block under with expected(AttributeError):
runs, and expected() catches the named exception
and prints it. Context
Managers builds expected().
A type parameter expresses the connection. Declare the parameter in square brackets after the function name:
# generics.py
def first[T](items: list[T]) -> T:
return items[0]
n = first([10, 20, 30]) # T is int
print(n + 1)
#: 11
# ty: "int" has no attribute "upper":
# n.upper()
s = first(["a", "b"]) # T is str
print(s.upper())
#: AT is a placeholder, filled in separately at each
call. The type checker infers T from the argument
and substitutes it into the return type. In first_any.py the same
n.upper() call type-checks and then fails at
runtime.
A class declares type parameters in square brackets after the class name:
# generic_box.py
class Box[T]:
def __init__(self, content: T) -> None:
self.content = content
def get(self) -> T:
return self.content
box = Box("gift") # A Box[str]
print(box.get().upper())
#: GIFTA bound limits the
parameter to one hierarchy. class Box[T: Shape]
accepts Shape and its subclasses. T
keeps the subclass the caller passes. A constraint
lists unrelated choices. With [T: (int, str)],
T is int or str. An
argument whose class subclasses int binds
T to int, where a bound keeps the
subclass.
A list[Circle] is not a
list[Shape], and that surprises most people the
first time:
# variance.py
from collections.abc import Sequence
class Shape:
pass
class Circle(Shape):
pass
class Square(Shape):
pass
def count(shapes: Sequence[Shape]) -> int:
return len(shapes)
def add_square(shapes: list[Shape]) -> None:
shapes.append(Square())
circles: list[Circle] = [Circle(), Circle()]
print(count(circles))
#: 2
# ty: expected "list[Shape]", found "list[Circle]":
# add_square(circles)A list accepts
writes. If you pass circles,
add_square() appends a Square to a
list declared as list[Circle]. The type checker
refuses the call, so the declaration list[Circle]
stays true. A read-only container is safe to pass this way, so
Sequence[Shape] accepts a
list[Circle]. A Sequence[Shape]
parameter declares that the function reads its argument and
leaves it as it was. That declaration lets the function accept
arguments that a list[Shape] parameter rejects. A
list[T] is invariant in T,
and a Sequence[T] is covariant.
A type parameter can carry a default, and the type checker uses it when an annotation specifies the class without its brackets:
# type_defaults.py
from typing import reveal_type
class Stack[T = str]:
def __init__(self) -> None:
self.items: list[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def top(self) -> T:
return self.items[-1]
words: Stack = Stack() # No brackets, so T is str
words.push("beta")
reveal_type(words.top()) # ty: str
print(words.top().upper())
#: BETA
counts: Stack[int] = Stack()
counts.push(2)
print(counts.top() + 1)
#: 3A default matters most for a class whose type parameter is usually the same type. Callers who use that type omit the brackets and still get a precise annotation.
Without the default, the annotation words: Stack
leaves T unsolved (see exercise 5).
A type alias declares
a default with = inside its brackets, as
Pair shows:
# alias_default.py
type Pair[T = int] = tuple[T, T]
def is_origin(point: Pair) -> bool: # Pair means Pair[int]
return point == (0, 0)
print(is_origin((0, 0)))
#: TrueDefaulted parameters go last, the way defaulted function
parameters do, so class Table[K = str, V] is a
syntax error. Python 3.13 added type parameter defaults, one
release after the bracket syntax.
**P and the Older
TypeVar SyntaxA special form, **P, captures the types of an
entire parameter list. Decorators
uses **P to give a wrapper the same signature as
the function it wraps.
Before Python 3.12 you wrote type
parameters with TypeVar and Generic,
which you still see in older code. The older form creates each
type parameter as a module-level TypeVar object and
lists Generic[T] among a generic class’s bases:
from typing import Generic, TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
class Box(Generic[T]):
def __init__(self, content: T) -> None:
self.content = contentThese first() and Box mean the same
as the bracket versions in generics.py and generic_box.py. The
bracket form declares T in the function or class
header, so T exists only inside that function or
class.
Self Return TypeA method that returns its own instance allows call chaining.
What should the type hint be? Specifying the enclosing class
works until someone inherits from it. Self means
“an instance of the class on which you called this method,” so
in a subclass it means the subclass:
# self_type.py
from typing import Self
class Tally:
def __init__(self) -> None:
self.count = 0
def bump(self) -> Self:
self.count += 1
return self
class NamedTally(Tally):
def __init__(self, name: str) -> None:
super().__init__()
self.name = name
def report(self) -> str:
return f"{self.name}: {self.count}"
t = NamedTally("clicks")
print(t.bump().bump().report())
#: clicks: 2t.bump() runs on a NamedTally, so
Self is NamedTally, and the result has
report(). If bump() declares
-> Tally, the type checker rejects
report(), because Tally has no such
method.
Alternative constructors need
Self for the same reason bump() does.
A @classmethod that ends with
return cls(...) declares -> Self,
so a call on a subclass has the subclass’s type, not the base’s
type.
**kwargs with a
TypedDictA **kwargs parameter
collects keyword arguments into a dictionary. Annotating it as
**kwargs: int says that every value is an
int, but it leaves open which keywords a caller may
pass. A TypedDict describes a dictionary with
specific keys, each with its own value type. Since Python 3.12
(PEP 692), Unpack[...] applies that description to
**kwargs, so the type checker treats each key as a
named, typed keyword parameter:
# typed_kwargs.py
from typing import NotRequired, TypedDict, Unpack
class Style(TypedDict):
width: int
fill: NotRequired[str]
def label(text: str, **style: Unpack[Style]) -> str:
fill = style.get("fill", " ")
return f"[{text.center(style['width'], fill)}]"
print(label("ok", width=6))
#: [ ok ]
print(label("ok", width=6, fill="*"))
#: [**ok**]Inside label(),
style is a Style dictionary.
Style requires width, so
label() reads style['width'].
NotRequired makes fill optional, so
label() reads it with .get() and a
default.
ReadOnly, added in Python 3.13 (PEP 705), is
another per-key qualifier. The type checker accepts a read of a
ReadOnly key and rejects an assignment to it. If
Style declares width: ReadOnly[int],
style['width'] = 8 inside label()
draws invalid-assignment, with the message that the
key “is marked read-only.” At runtime the assignment succeeds,
since every TypedDict value is a
dict.
The type checker verifies the
keywords at each call. A call that passes width="6"
draws invalid-argument-type from ty,
the same diagnostic a wrongly typed ordinary parameter draws.
The diagnostic’s message reads “Expected int, found
Literal["6"]”. A call that leaves out
width draws missing-argument.
The checkers disagree about a misspelled keyword. Pyright
rejects fill misspelled as fil="*"
with No parameter named "fil", but ty
0.0.95 accepts that call silently. At runtime fil
becomes an extra key in style. label()
ignores that key, so the fill stays a space.
A TypedDict is open
by default. It lists the keys a dictionary must have, so a
TypedDict with more keys still satisfies it. Python
3.15 (PEP 728) adds closed TypedDicts,
which permit only their declared keys. Declaring
class Style(TypedDict, closed=True): makes
ty reject fil="*" with
unknown-argument. Writing
extra_items=str instead of closed=True
admits undeclared keys and requires a str for each
of their values.
A TypedDict helps most when several functions
accept the same options. One TypedDict declares
them once, so every signature that unpacks it accepts the same
keywords.
Type hints do not change what the program does. Python stores them and otherwise ignores them. A wrongly typed value that gets past the type checker runs as it does in a program with no hints. Checking is a separate step you run, the same way you run tests. The hints are for the tools and for the reader.
If you need a runtime guarantee, use
isinstance() or a library that validates data. The
typeguard library
reads your existing annotations and enforces them at runtime. Pydantic validates and
parses data against typed models. Pydantic suits the edges of a
program, where untrusted input enters.
From here on, this book assumes the type checker runs on everything. When a listing says the type checker rejects a line, that rejection is the only enforcement.
Gradual typing leaves the amount up to you, so the question is when a hint is worth the words. Annotate what crosses a boundary: function signatures, public attributes, anything another file imports. Those are the places where the code’s reader and writer are different people, and where a wrong assumption and the failure it causes sit farthest apart.
Let the type checker infer the
rest. For a local variable whose type is obvious from its
initializer, a hint repeats what the initializer says.
count: int = 0 says the same as
count = 0, at greater length. (The
total: int = 0 in this chapter’s first listing
shows the syntax, not a recommendation.)
A hint helps in proportion to the distance between a value’s creation and its use. A value created and used within three lines can leave its type to inference. A value that arrives from another module, inside a container, needs a precise annotation.
These are the type hints you encounter, in their modern forms. Each subsection opens with a link to the associated Python documentation. Thinking in Types explores types in more depth.
Type hints go in three places: a parameter
(x: int), a return value (-> str),
and a variable or attribute (total: int = 0). Most
of the names below come from the typing module. The
abstract container types come from
collections.abc.
Python documentation: basic types
| Construct | Meaning |
|---|---|
int, str, float,
bool, bytes, complex |
The built-in types, annotated by name alone, with no type
parameter; the type checker accepts an int where a
declaration says float, and an int or
float where it says complex, but not
the reverse (bytes and complex are not
used as annotations elsewhere in this book) |
None |
The value None; the return type of a function
that returns nothing |
object |
Any object, with the methods every class shares and no
others (safer than Any) |
Any |
Opts out of checking; compatible with every type, see Gradual Typing |
Never, NoReturn |
The “impossible” type, which no value has;
NoReturn marks a function that never returns (it
always raises an exception or exits), and Never is
the same type under a name that also suits other positions |
LiteralString |
A str built only from literals, for
injection-sensitive APIs |
Python documentation: containers
| Construct | Meaning |
|---|---|
list[T], set[T] |
A homogeneous collection of T; invariant, so
list[Circle] is not a list[Shape], see
Variance |
frozenset[T] |
An immutable homogeneous set of T; covariant,
because it has no method that adds an element, so a
frozenset[Circle] satisfies
frozenset[Shape], see Variance |
dict[K, V] |
A dictionary with keys K and values
V, see Type Hints |
tuple[A, B] |
A fixed-length tuple (here a pair), see Type Hints |
tuple[T, ...] |
A variable-length tuple of T, see Type Hints |
Sequence[T], Iterable[T],
Iterator[T], Mapping[K, V] |
Read-only abstract shapes from collections.abc;
covariant in their element type, so list[Circle]
satisfies Sequence[Shape]
(Mapping[K, V]’s K stays invariant),
see Variance |
Generator[Y, S, R] |
A generator’s yield, send, and return types;
Iterator[T] is enough when it only produces values,
see Generators |
Callable[[A, B], R] |
A function taking A, B and
returning R (... for any
parameters) |
type[C] |
The class object C, not an instance of it, see
Classes as Values |
TypeForm[T] |
A type expression passed as a value, such as a union, see Classes as Values |
Python documentation: unions, optionals, and literals
| Construct | Meaning |
|---|---|
X | Y |
A union: either type, see Type Hints |
X | None |
Optional: X or None, see Type Hints |
Literal[...] |
One of a closed set of constant values,
e.g. Literal["r", "w"], see The type
Statement |
Python documentation: aliases and distinct types
| Construct | Meaning |
|---|---|
type Name = ... |
A name for a type, compound or scalar,
e.g. type Coord = tuple[int, int], see The type
Statement |
NewType("Id", int) |
A distinct type, int at runtime but separate to
the type checker; the base can be any class, not just a
built-in |
Annotated[T, meta] |
T carrying extra metadata for libraries and
tools |
Python documentation: constants and class variables
| Construct | Meaning |
|---|---|
Final, Final[T] |
A name the type checker protects from reassignment, see Constants with Final |
ClassVar[T] |
A class-level attribute, not one per instance, see Class Attributes |
Python documentation: generics
| Construct | Meaning |
|---|---|
def f[T](x: T) -> T |
A generic function (the type parameter varies per call), see Generic Functions and Classes |
class Box[T] |
A generic class, see Generic Functions and Classes |
[T: Base], [T: (int, str)] |
A bounded or constrained type parameter, see Type Parameters |
[T = str] |
A type parameter default, used when you omit the brackets, see Type Parameter Defaults |
TypeVar, Generic[T] |
The pre-3.12 way to write type parameters, see **P and the Older
TypeVar Syntax |
**P (ParamSpec) |
Captures a callable’s parameter list including types, for decorators, see Decorators |
*Ts (TypeVarTuple),
Unpack, Concatenate |
Variadic generics and parameter manipulation |
Python documentation: structural typing
| Construct | Meaning |
|---|---|
Protocol |
A required shape (methods and attributes), satisfied without inheritance, see Structural Typing with Protocols |
@runtime_checkable |
Allows isinstance() against a
Protocol, see Surrogate |
Python documentation: dictionary and record shapes
| Construct | Meaning |
|---|---|
TypedDict |
A dict with specific keys and value types;
**kwargs: Unpack[TD] types keyword arguments with
one, see Typing **kwargs
with a TypedDict |
Required[...], NotRequired[...],
ReadOnly[...] |
Per-key control inside a TypedDict, see Typing **kwargs with a
TypedDict |
closed=True, extra_items=T |
Class arguments that limit a TypedDict to its
declared keys, or type the values of the others, see Typing **kwargs with a
TypedDict |
NamedTuple |
A typed, named tuple class, see Data Transfer Objects |
Python documentation: type narrowing
| Construct | Meaning |
|---|---|
TypeGuard[T], TypeIs[T] |
A boolean predicate that narrows a type:
TypeGuard narrows only where it returns
True, TypeIs narrows both
branches |
Python documentation: self and forward references
| Construct | Meaning |
|---|---|
Self |
The enclosing class type; useful for fluent methods and
alternative constructors, see The
Self Return Type |
"Name" |
A forward reference to a not-yet-defined type; quoting is optional under deferred evaluation (PEP 649), see Composite and Interpreter |
Python documentation: typing decorators and directives
| Construct | Meaning |
|---|---|
@overload |
Several typed signatures for one function name |
@override |
Declares that a method overrides a base-class method, see Classes |
@final |
Forbids subclassing the class, or overriding the method, see Metaprogramming |
cast(T, x) |
Tells the type checker to treat x as
T; Flyweight
shows the runtime guard to prefer over it |
assert_never(x),
assert_type(x, T), reveal_type(x) |
Type-checker assertions and aids;
assert_never() shown in Pattern
Matching |
TYPE_CHECKING |
A flag that is True only to the type checker,
for type-only imports, see Modules
and Packages |
These decorators and directives
exist for the type checker and the reader. At runtime most of
them return their argument. Two act when a call reaches them:
assert_never() raises an
AssertionError, and reveal_type()
prints its argument’s runtime type.
Older code writes some of the constructs in these tables
differently: Optional[X] for X | None,
Union[X, Y] for X | Y, and
List, Dict, Set,
Tuple from typing for the lowercase
built-ins. The forms above are the modern ones.
Try each exercise before opening its solution.
protocols.py, add a class
Triangle with its own draw(), and pass
an instance to render() without changing
Drawable or render().area.py, remove the
# type: ignore comment and run
ty check on the file. Read the error, then restore
the comment.generics.py, write a
second generic function,
last[T](items: list[T]) -> T, that returns the
final element, and call it on both a list[int] and
a list[str] the way the listing calls
first().self_type.py, add a
subclass of NamedTally called
LoudTally whose report() returns the
message in all capitals, calling super().report()
first. Confirm that ty check accepts
.bump().bump().report() on a LoudTally
and that the call prints the message in capitals.ty check on type_defaults.py and read
what it reports for reveal_type(words.top()).
Remove the = str default and run it again. The type
checker accepts the file either way. Say what that means for a
bare Stack annotation.type_aliases.py, call
paint(grid, (2, 3), "purple") and run
ty check. Read the error, then widen
Color to admit "purple" and confirm
the error goes away.variance.py, change
add_square()’s parameter annotation to
Sequence[Shape] and uncomment the call. Explain why
the type checker now accepts the call and why
shapes.append(...) no longer type-checks.narrowing.py, replace
if text is not None: with if text: and
run ty check. Explain why the empty string now
takes the other branch even though the type checker accepts
either version.narrowing_attribute.py,
copy b.val into a local variable before the
if and use the local inside it. Replace the
expect() call with print(show(box))
for a Box named box, then print
box.val. Explain why the
AttributeError disappears although
reset() still runs.final_constants.py,
annotate HISTORY as
Final[tuple[str, ...]], start it as an empty tuple,
and run ty check. Read what the type checker
reports for the append() call. Then change the
listing so HISTORY holds "first", and
explain why the entry must go into the definition of
HISTORY.