PythonStatic Type Checking (mypy)

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.

Note
Type hints are inert at runtime — Python never checks them when your program executes. All of the actual enforcement described below happens in a separate static-analysis step, before the code runs.
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.

Bash
pip install mypy
mypy file.py
Worked example: catching a real bug

Python
# 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

Python
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-defs once most functions have hints.

  • Mix typed and untyped code indefinitely — there's no all-or-nothing switch you must flip.

TOML
[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.

Tip
Fixing a type error while you're still writing the function is cheaper than discovering it later in a mypy run or, worse, in production — let your editor's inline type checker be your first line of defense, and mypy in CI be the safety net that catches anything the editor missed.