The Static Typing Ecosystem
In short: def greet(name: str, times: int) -> str: is a type hint —
Python writes it down, stores it in fn.__annotations__, and then does
nothing else with it. A hint never stops a call, never converts a value,
never raises on its own. Catching a mismatch before the program runs is a
separate tool's job: mypy or pyright read your hints and your calls
and report a mismatch without running anything. Pydantic takes the
opposite approach — it checks types at runtime, when real data (JSON from
an API, a form submission) arrives untyped. typing.get_type_hints(fn) and
fn.__annotations__ both run in the browser; mypy, pyright and Pydantic are
read-only here, since none of them can run inside Pyodide.

Hint a function's shape
A hint documents what a function expects, for a human reading it and for a tool checking it — never for Python itself:
Both show the same three entries: name, times, and return. typing
adds vocabulary for shapes a bare class can't express: list[int] (a list of
ints), dict[str, int] (string keys, int values), int | None (an int,
or nothing — the modern spelling of Optional[int]), and TypedDict for a
dict with a fixed, named set of keys, each with its own type. None of these
change what the function does — only what a reader, or a checker, can tell
about it:
def greet(name: str, times: int) -> str:
return (name + " ") * times
print(greet("hi", "3"))Catch a bug before it ships
mypy and pyright read your source without running it, walk every call, and check each argument against the function's hints. Point either one at the file above and it reports the mismatch before anything executes:
$ mypy app.py
app.py:5: error: Argument 2 to "greet" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)
$ pyright app.py
app.py:5:18 - error: Argument of type "Literal['3']" is not assignable to parameter "times"
of type "int" (reportArgumentType)
1 error, 0 warnings, 0 informationsSame bug, same line, caught at two different moments: mypy/pyright catch it before the program runs at all; without them, Python only finds out when that exact line executes — a code path a quick test run might never reach.
Validate data at the door
Hints describe shape to a checker. Pydantic goes further and actually enforces a shape at runtime, which matters most at the edges of a program — data arriving from a request body, a config file, a database row, none of which ever ran through mypy:
from pydantic import BaseModel
class Order(BaseModel):
item: str
quantity: int
Order(item="Pens", quantity=3) # fine — builds the object
Order(item="Pens", quantity="three") # raises ValidationError, right here, at runtime"three" can't become an int, so Pydantic raises immediately — the one
place in this lesson a type mismatch actually stops a program on its own.
That's the trade mypy/pyright don't make: no setup, no separate checking
step, but the cost is paid on every real call instead of once before
shipping.
Three more names worth knowing by what they're for, not what they check:
pyenv installs and switches between Python versions themselves (3.11 vs
3.12) — nothing to do with typing. Pipenv and virtualenv manage
per-project environments, the same job venv does (section-15 covers
that in full) — Pipenv adds a lockfile on top. pyre is Meta's own static
checker, a third alternative to mypy and pyright, same idea, different tool.
Quick practice
def area(width: float, height: float) -> float:
return width * height
print(area(3, 4), type(area(3, 4)))def ship(weight: "int"):
pass
print(ship.__annotations__)mypy reports an error on a line that Python runs without crashing every single time in production. What does that mean?
from typing import Optional
def find_user(id: int) -> Optional[str]:
return None
result = find_user(5)
print(result is None, result == "")🎯 Your turn
Write type_check(args). It checks a fixed function, safe_divide(a: int, b: int) -> float, against the two values in args (positionally: args[0] is
a, args[1] is b). Use safe_divide.__annotations__ and
safe_divide.__code__.co_varnames to find each parameter's expected type,
and isinstance to check the matching value. Return True only if every
value matches its hint.
type_check([10, 2])→Truetype_check([10, "2"])→False
Hint: names = safe_divide.__code__.co_varnames[:safe_divide.__code__.co_argcount]
gives you ("a", "b") in order; zip(names, args) pairs each one with its
value.
Press ▶ Run, try the Live App chips, then ✓ Check. After that, the practice set below builds a small runtime validator out of the same annotation-reading idea.
