Static Type Checking (mypy)
Python lets you annotate variables, function parameters, and return values with types — def add(a: int, b: int) -> int:. But Python the language does essentially nothing with these hints at runtime: it does not enforce them, does not raise an error if you pass a string where an int is annotated, and happily runs code with completely wrong annotations. Type hints are, by themselves, just documentation that an editor or a separate tool can read. That separate tool, for most Python projects, is mypy.
What mypy does
mypy is a static type checker — it reads your source code without executing it and checks that the way you use values is consistent with the type hints you (and the libraries you depend on) wrote. If you annotate a parameter as int and then pass it a str somewhere in your code, mypy reports that as an error before you ever run the program.
pip install mypy mypy file.py
Worked example: catching a real bug
# billing.py
def apply_discount(price: int, percent: int) -> float:
return price - (price * percent / 100)
def format_receipt(total: float) -> str:
return f"Total: ${total:.2f}"
final_price = apply_discount("49.99", 10)
print(format_receipt(final_price))This code runs without crashing — Python doesn't care that "49.99" is a string, not an int, until the arithmetic inside apply_discount actually fails or silently misbehaves. Running mypy against it catches the mistake immediately:
$ mypy billing.py billing.py:9: error: Argument 1 to "apply_discount" has incompatible type "str"; expected "int" [arg-type] Found 1 error in 1 file (checked 1 source file)
mypy points to the exact line and explains precisely which argument has the wrong type and what type was expected — far more actionable than discovering it as a TypeError (or, worse, silently wrong output) after shipping to production.
Another common case: calling a method that doesn't exist
def get_user_id(user: dict) -> int:
return user.get_id() # dict has no method called get_id$ mypy users.py users.py:2: error: "dict[Any, Any]" has no attribute "get_id" [attr-defined] Found 1 error in 1 file (checked 1 source file)
Gradual typing
mypy is built around gradual typing: you are never required to annotate an entire codebase before getting any value from it. Untyped functions and modules remain perfectly valid Python and mypy will not complain about them by default. This means you can:
Start by running mypy on a codebase with zero type hints — it will simply report far fewer errors, since untyped code is treated as implicitly
Any(anything goes).Add hints to one file, or one module, at a time — mypy checks whatever is annotated and leaves the rest alone.
Gradually tighten strictness as coverage improves, e.g. enabling
--disallow-untyped-defsonce most functions have hints.Mix typed and untyped code indefinitely — there's no all-or-nothing switch you must flip.
[tool.mypy] python_version = "3.11" warn_unused_ignores = true warn_redundant_casts = true # start lenient, then tighten over time: # disallow_untyped_defs = true # strict = true
IDE integration
You don't have to wait for a terminal run of mypy to see type errors. Modern editors run a type checker in the background and underline problems as you type, using the same type-hint information mypy reads.
VS Code with the Pylance extension (built on Microsoft's Pyright) underlines type errors inline and shows inferred types on hover.
PyCharm has a built-in type checker that highlights incompatible types and missing arguments as you write code.