Skip to content

Static Type Checking with mypy

Type hints sit in the source code, but Python ignores them when it runs. To get any value out of them, you need a tool that reads the annotations and tells you when they don't line up. That tool is a type checker, and the standard one is mypy.

Install and Run

mypy is a development dependency. It's not needed at runtime:

shell
uv add --dev mypy

Run it against the project root:

shell
uv run mypy .

On a clean project you'll see Success: no issues found in N source files. When something's off, mypy points at the file, the line, and the mismatch.

Catching a Mismatch

Say we wrote slugify in slug.py with the wrong return type:

Python
def slugify(name: str) -> int:
    return name.strip().lower().replace(" ", "-")

Running mypy:

Text
$ uv run mypy .

slug.py:2: error: Incompatible return value type
    (got "str", expected "int")  [return-value]
Found 1 error in 1 file (checked 1 source file)

The function signature claims it returns an int, the body returns a string, and mypy catches it before the program ever runs. Same goes for callers passing the wrong types in, missing arguments, accessing attributes that don't exist on a class, and so on.

Configuring mypy

mypy reads its configuration from pyproject.toml under [tool.mypy].

It might look something like:

TOML
[tool.mypy]
strict = true
files = ["src"]

strict = true turns on the full set of strict-mode flags in one switch (no implicit Any, complete function annotations required, and so on). files tells mypy which directories to check by default, so you can run uv run mypy without arguments.


Marking a Package as Typed

Type hints in your own code help you. If you publish a package to PyPI, hints help the people who install it too, but only if their type checker knows your package ships with annotations.

PEP 561: to declare that your package supports static typing, place an empty file named py.typed in the package root. The file is purposefully empty and used as a flag.

Info

Without py.typed, mypy treats your package as untyped from the outside and silently skips its annotations when other projects depend on it. The marker is a one-line fix that helps downstream users of your package.