How do I catch type errors before running my Python code?
Short answer
Add type hints and run mypy, which reads them and reports mismatches without executing the code. It catches a whole class of bug that tests may miss, especially around None.
The short version
Add type hints to your function signatures and run mypy, which reads them and reports mismatches statically, without running the code. It catches a family of bugs that tests often miss, above all the ones involving None reaching a place that cannot handle it.
Running it
pip install mypy
mypy src/
Mypy reads your annotations and follows the types through the code, reporting where they do not line up.
def add(a: int, b: int) -> int:
return a + b
add("x", "y")
# error: Argument 1 to "add" has incompatible type "str"; expected "int"
Python itself would run this happily and concatenate the strings. Mypy catches it before you ever execute the file.
Where it really pays: None
The most valuable checks concern values that might be None.
def find_city(code: str) -> str | None:
...
name = find_city("LDS")
print(name.upper())
# error: Item "None" of "str | None" has no attribute "upper"
This is the classic AttributeError: 'NoneType' object has no attribute ... that appears at three in the morning in production. Mypy finds it at your desk, because the signature admits that find_city might return nothing and the code does not handle that case.
The fix is to handle it, which mypy then accepts.
if name is not None:
print(name.upper())
It only checks what you annotate
Mypy is a gradual system: unannotated code is largely skipped. So the value you get is proportional to the hints you provide. Start with function signatures at your module boundaries, which is where mistakes cross between components and where the payoff is highest.
Useful strictness settings
Configure it in pyproject.toml so everyone gets the same behaviour.
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_ignores = true
Adding disallow_untyped_defs = true requires every function to be annotated, which is a good target for a mature codebase but too aggressive to switch on at the start of an existing project.
What it cannot do
Mypy checks types, not correctness. It will not notice that you summed the wrong column, joined on the wrong key, or wrote a statistically invalid test. It removes a category of mechanical error so that your attention and your tests can go to the logic, which is the part that matters.
Third-party libraries
Some libraries ship type information and some do not. For those that do not, mypy treats their types as unknown, which limits what it can check. Stub packages (like pandas-stubs) fill this gap for common libraries and are worth installing, since data code spends most of its time inside them.