A value has a type whether or not you write it down, and a mistake about it surfaces when the operation runs. Static types let a tool find that mistake before the program does.
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 almost no type declarations, and on a small program you may not miss them.
Python 3.5 (2015) introduced type hints, 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 something reads the annotations.
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, and Pyright
is the one most editors run. This book uses Astral’s ty
instead, from the same group that makes uv and
ruff. Pyright checks the same listings as a second
opinion, and the book’s build does not depend on its verdict.
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
annotations still works. The type checker treats it 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
earn their keep: the public interfaces, the tricky data, the
code on which other people depend.
An explicit Any indicates that a value is truly
dynamic. ty reports the Any that comes
from a missing annotation as Unknown, to
distinguish it from an Any you wrote yourself. They
behave the same. Both are compatible with everything.
Any and object differ. Both accept
every value, but object guarantees nothing about
the value once you have it, so the type checker rejects every
operation beyond object’s own. Any
permits every operation instead, so it opts out of checking
rather than describing a wide set of values. object
is the base of every class, so the type checker treats an
object value as a checked value with the methods
every class shares, while Any switches the checker
off for that one value. Choose object for a value
you store or pass along untouched, and Any for one
whose operations you would rather check by hand.
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 names 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 any
number of ints. A function that returns nothing
declares -> None, and that is why every
__init__() in this chapter’s listings carries that
annotation.
tyThe hints do nothing on their own. You need a tool to check them:
ty check
uvx ty check runs it without installing
anything, and uv tool install ty@latest puts it on
your path.
ty complains where the hints and the code
disagree, and stays quiet 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) runs without error. 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 discovers the problem immediately.
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: the diagnostic for a line that would
fail the check, whether commented out or suppressed with
# type: ignore, or the type a
reveal_type() call produces.
reveal_type(), imported from typing,
asks the type checker which type it inferred for an expression,
and ty check prints the answer as a diagnostic.
The # ty: summary is for you, not for the tool.
ty acts on a comment that has the form
# type: ignore, which silences every diagnostic on
its line, or # ty: ignore[invalid-argument-type],
which 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, and not just 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 .upper() needs no cast.
Outside the if, text is still the full
str | None. A truthiness test,
if text:, narrows the same way, since
None is falsy, but it sends the empty string to the
other branch along with None, so test
is not None when an empty value counts as supplied
(see exercise 8). The same narrowing follows an
isinstance() check, an equality test, or an
identity test against a specific value such as
is not SOME_SENTINEL.
An object parameter depends on narrowing. Gradual Typing says the type checker
rejects every operation on an object beyond
object’s own, so the 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))
#: 42Inside the if, value is a
str, so upper() checks.
repr() accepts every object, so the
return at [1] needs no test. With
Any in place of object, the
commented-out line checks too, and describe(42)
then 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, and the type checker has no way to see inside that call:
# 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 nothing in
the checker’s model connects reset() to that
narrowing, so the checker keeps b.val narrowed to
str. The AttributeError shows the
narrowing was stale by the time upper() ran.
A narrowing on a local variable holds. A narrowing on an attribute can go stale, so recheck the attribute after any call that might touch the object. Copying the attribute into a local variable before the test avoids the recheck, because a function defined elsewhere cannot rebind its caller’s local variable (see exercise 9).
Final on a name makes the type checker catch 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, and 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']Final blocks rebinding the name, not mutation of
the object the name holds. HISTORY.append("first")
checks and runs, the same as it does on a non-Final
list. The type checker refuses only an assignment to the name
HISTORY. Final invites a
misconception. The word suggests immutability, but the object
stays mutable.
You can give the type explicitly, 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 ignores whatever
goes into the list. CACHE: Final[list[str]] = []
says what the list holds, and the type checker enforces it.
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. Instead of waiting until the program runs, 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 trusts the object once the code is running, while structural typing proves the shape beforehand.
A Protocol expresses shape. Some statically typed
languages make you declare up front that a class “is a”
Drawable by inheriting from it. A
Protocol instead describes a required shape. Any
object with that shape qualifies, without inheriting from a base
class:
# 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() that takes no arguments and
returns a 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, does not match.
A Protocol is a checking-time construct, so
isinstance(Circle(), Drawable) raises a
TypeError instead of answering. Decorating the
Protocol with @runtime_checkable allows the call,
at the cost of a weaker
check.
Drawable appears in one place, the annotation on
render()’s parameter. If you pass an object without
a draw() to render(), the type checker
rejects the call. Blob is the case worth watching.
It draws, in the everyday sense, but the method’s name is
paint(), and a protocol matches on names and
signatures rather than on intent. Protocols preserve the
flexibility of dynamic typing but add the early warning of
static type checking.
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
class Shape:
pass
class Circle(Shape):
pass
def make(kind: type[Shape]) -> Shape:
return kind()
shape = make(Circle)
print(type(shape).__name__)
#: Circlemake() takes the class, not an instance, so the
argument’s annotation is type[Shape]. Passing
Circle works because Circle is a
subclass of Shape. Calling kind() then
produces an instance.
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.
type
StatementAn annotation can grow to the point of obscurity.
dict[tuple[int, int], str] is precise, but it does
not say 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 Grid = dict[Coord, str]
type Color = Literal["red", "blue", "green", "yellow"]
def paint(grid: Grid, cell: Coord, color: Color) -> None:
grid[cell] = color
grid: Grid = {}
paint(grid, (2, 3), "red")
print(grid)
#: {(2, 3): 'red'}Like 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
type(grid) in the same file returns
dict as it always has.
A type alias is 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. (For a type the type checker keeps
separate from its base, use NewType.)
Because an alias creates no new type, save it for a compound
shape instead of using it to rename a built-in.
type UserId = int looks like a new type in a
signature while behaving like int.
Color names 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 is the lightest way to close a
set of values. Once 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. This function works on 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, and the return type
then says nothing about what 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.nonexistent_method()
#: [AttributeError] 'int' object has no attribute
#: 'nonexistent_method'ty check passes this file with no complaint.
n is Any, so every attribute access on
it type-checks, including one that runs and fails.
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
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 then knows the return type. Both n + 1 and
s.upper() pass the type checker, while
n.upper() fails.
A class declares type parameters the same way:
# 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())
#: GIFTConstructing Box("gift") binds T to
str for that instance, so get()
returns a str and the call to upper()
checks.
A bound limits the parameter to one hierarchy:
class Box[T: Shape] accepts Shape and
its subclasses, and T keeps the subclass it was
given. A constraint lists unrelated choices: with
[T: (int, str)], T is int
or str, and an argument of a subclass of
int binds T to int.
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 its caller believes holds only
circles. The type checker refuses the call to prevent that. A
read-only container has no such problem, so
Sequence[Shape] accepts a
list[Circle]. Annotating a parameter
Sequence[T] instead of list[T]
declares that the function reads its argument and leaves it as
it was, so the function accepts arguments that a
list[T] 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 names the class without its brackets:
# type_defaults.py
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")
print(words.top().upper())
#: BETA
counts: Stack[int] = Stack()
counts.push(2)
print(counts.top() + 1)
#: 3words: Stack names the class without its
brackets, and the default makes that annotation mean
Stack[str], so words.top() is a
str and upper() checks.
A default matters most for a class whose type parameter is usually the same type. Callers who use that type omit the brackets, and the annotation stays precise.
Queue is the same class without the default, so
a bare Queue annotation leaves T
unsolved:
# type_defaults_bare.py
from typing import reveal_type
class Queue[T]:
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]
line: Queue = Queue() # No brackets, T unsolved
line.push("first")
reveal_type(line.top()) # ty: Unknownty reports Unknown, not an error,
so line.top() and everything built on it go
unchecked from here (see exercise 5).
A type alias takes a default the same way, 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. Type parameter defaults arrived in Python 3.13,
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.
Self Return TypeA method that returns its own instance allows call chaining.
What should the type annotation be? Naming the enclosing class
works until someone inherits from it. Self means
“an instance of the class on which you called this method,” so
it automatically adapts to subclassing:
# 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 benefit the same way. A
@classmethod that ends with
return cls(...) returns Self, so a
call on a subclass produces an instance of that subclass, not of
the base.
**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 says
nothing about 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. width is required,
so style['width'] needs no guard.
NotRequired makes fill optional, so
label() reads it with .get() and a
default.
The type checker checks the keywords at each call. A call
that passes width="6" draws
invalid-argument-type from ty,
reporting “Expected int, found
Literal["6"]”, the same diagnostic a wrongly typed
ordinary parameter draws. A call that leaves out
width draws missing-argument.
A misspelled keyword is where the checkers disagree. Pyright
rejects fill misspelled as fil="*"
with No parameter named "fil", but ty
0.0.84 accepts that call without a diagnostic. At runtime
fil becomes an extra key in style, and
label() ignores that key, so the fill quietly stays
a space.
The payoff grows when several functions accept the same
options. One TypedDict declares them once, and
every signature that unpacks it stays in step.
Type hints do not change what the program does. Python stores
them and otherwise ignores them. A wrong type that slips past
the type checker behaves as it does without hints. Checking is a
separate step you run, the same way you run tests. 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, and that suits the edges of a
program, where untrusted input enters. The hints are for the
tools and for the reader.
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 travels farthest before it fails.
Let the type checker infer the rest. A local variable whose
type is obvious from its initializer gains nothing from an
annotation, and count: int = 0 says no more than
count = 0 does, at greater length. (The
total: int = 0 in this chapter’s first listing
shows the syntax, not a recommendation.) The value of a hint is
proportional to the distance between a value’s creation and its
use. A value born and consumed three lines later needs no help.
A value that arrives from another module, inside a container, is
worth annotating precisely.
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.
Annotations 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, but with no behavior assumed (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 nothing can be written into it, 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 |
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 type alias for a longer type,
e.g. type Grid = dict[tuple[int, int], str], 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 does not let you reassign, 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 |
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 Simulation |
These exist for the type checker and the reader, and at
runtime most of them do nothing. 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 them 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.
This chapter’s solutions give a hint, usually the shape of the code, and a full answer for each exercise.
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 .bump().bump().report() still chains
correctly on a LoudTally.reveal_type(words.top()) to type_defaults.py and run
ty check on the file. Remove the = str
default and run it again. The type checker reports no error
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.