What you'll learn
- Run
mypyon a file, read its report, and fix what it finds - Describe the shape of a dictionary with
TypedDict - Declare a behavioural contract without inheritance with
Protocol - Write generic functions and classes with the 3.12
[T]syntax so types flow through
Concept
mypy reads your hints and checks that every call, assignment, and return agrees with them, across every file, without running anything. It reports each disagreement with a file, a line, and a sentence. Run it before your tests and it catches a whole class of bugs, wrong argument order, a None that was not handled, a method called on the wrong type, in seconds rather than in production. The output is the tool's opinion, and when you disagree, you either fix the hint or fix the code; both are improvements.

The first thing a real codebase needs beyond the basics is a way to type dictionaries with known keys, which is what JSON gives you. TypedDict declares a dictionary shape: which keys exist and what type each value has. It is still a plain dict at runtime, with no overhead and no methods, but the checker now knows that user["name"] is a str and that user["nmae"] is a mistake. total=False or NotRequired[...] mark optional keys.
| Tool | Describes | Checked how | Runtime cost |
|---|---|---|---|
TypedDict |
a dictionary with specific keys and value types | statically | none; it is a plain dict |
Protocol |
"any object with these methods/attributes" | statically (runtime with @runtime_checkable) |
none |
generics [T] |
a function/class that works for any type, consistently | statically | none |
Literal["a", "b"] |
one of a fixed set of values | statically | none |
Protocol is duck typing made checkable. Declare a class with the methods and attributes you need, and any object that has them satisfies the protocol, with no inheritance and no import of your type by the other side. A function that takes Drawable accepts anything with a draw() method, and mypy verifies each caller's argument has one. This is the tool for describing what your function needs rather than what class it wants, and the standard library uses it throughout collections.abc.
Generics let types flow through code that does not care what it holds. def first(items: list[T]) -> T says "whatever the item type is, that is what comes back", so first(["a"]) is known to be a str and first([1]) an int. Since 3.12 the type parameter is declared in brackets after the name, def first[T](items: list[T]) -> T:, and classes take them the same way, class Stack[T]:. A bound, [T: Comparable], restricts what T may be. Before 3.12 this needed TypeVar, which you will still see in older code.

Code
A file with two deliberate type errors and its mypy report, then the fixed version and its clean report. The check runs the pinned mypy against the pinned Python.
# broken.py
def total(prices: list[float]) -> float:
return sum(prices)
def greet(name: str | None) -> str:
return "hello, " + name
print(total(["10", "5"]))
print(greet(None))
# check_broken.sh
mypy broken.py
broken.py:6: error: Unsupported operand types for + ("str" and "None") [operator]
broken.py:6: note: Right operand is of type "str | None"
broken.py:9: error: List item 0 has incompatible type "str"; expected "float" [list-item]
broken.py:9: error: List item 1 has incompatible type "str"; expected "float" [list-item]
Found 3 errors in 1 file (checked 1 source file)# fixed.py
def total(prices: list[float]) -> float:
return sum(prices)
def greet(name: str | None) -> str:
return "hello, " + (name if name is not None else "stranger")
print(total([10.0, 5.0]))
print(greet(None))
15.0
hello, stranger# check_fixed.sh
mypy fixed.py
Success: no issues found in 1 source fileTypedDict for JSON-shaped data and Protocol for a behavioural contract. Both are ordinary at runtime; the shell block shows mypy catching the misuse of each.
# shapes.py
from typing import NotRequired, Protocol, TypedDict
class User(TypedDict):
id: int
name: str
email: NotRequired[str]
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "()"
class Square:
def draw(self) -> str:
return "[]"
def render(shapes: list[Drawable]) -> str:
return " ".join(s.draw() for s in shapes)
def display(user: User) -> str:
return f"{user['id']}: {user['name']} <{user.get('email', 'no email')}>"
ada: User = {"id": 1, "name": "ada"}
print(display(ada))
print(render([Circle(), Square()]))
1: ada <no email>
() []# shapes_wrong.py
from shapes import User, render, display
bad_user: User = {"id": "one", "name": "ada"}
print(display({"id": 2, "nmae": "linus"}))
print(render(["not drawable"]))
# check_shapes.sh
mypy shapes_wrong.py
shapes_wrong.py:3: error: Incompatible types (expression has type "str", TypedDict item "id" has type "int") [typeddict-item]
shapes_wrong.py:4: error: Missing key "name" for TypedDict "User" [typeddict-item]
shapes_wrong.py:4: error: Extra key "nmae" for TypedDict "User" [typeddict-unknown-key]
shapes_wrong.py:5: error: List item 0 has incompatible type "str"; expected "Drawable" [list-item]
Found 4 errors in 1 file (checked 1 source file)Generics with the 3.12 syntax: a function whose return type follows its argument, and a Stack[T] class that mypy keeps consistent.
# generics.py
from collections.abc import Iterable
def first[T](items: Iterable[T]) -> T | None:
for item in items:
return item
return None
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def __len__(self) -> int:
return len(self._items)
def largest[T: (int, float, str)](values: list[T]) -> T:
return max(values)
s: Stack[int] = Stack()
s.push(1)
s.push(2)
print(s.pop(), len(s))
print(first(["a", "b"]), first([]), first(range(5, 9)))
print(largest([3, 9, 4]), largest(["pear", "fig"]))
2 1
a None 5
9 pearTry it yourself
Take typed_inventory.py from the previous lesson and run mypy on it; fix anything it reports. Then add a TypedDict for the JSON shape a product arrives in, a Protocol called Priced with a price attribute, a generic def cheapest[T: Priced](items: list[T]) -> T, and a deliberate mistake in a caller. Run mypy again, read the message, and fix it.
Common mistakes
- Silencing errors with
# type: ignoreorAny. Each one hides a real disagreement; fix the hint or the code, and reserveignorefor library quirks you have understood. - Using a
TypedDictwhere a dataclass fits. If the data is yours, a dataclass gives methods and validation;TypedDictis for shapes that arrive as dictionaries. - Inheriting from a
Protocolto satisfy it. That works but is unnecessary; the whole point is that any object with the methods qualifies.
What's next
mypy file.pychecks every call and assignment against the hints and reportsfile:line: message; fix, re-run, repeat.TypedDicttypes dictionaries with known keys;Protocoltypes behavior without inheritance.def f[T](x: T) -> Tandclass Box[T]keep types consistent through generic code;[T: bound]restricts the parameter.
Your code is checked before it runs. Next comes a tool for text that every developer needs and few enjoy: regular expressions, done carefully.