Skip to Content
Python Advanced: Code That Scales Past Your Own Laptop
Advanced
0 likes
Courses
Python Advanced: Code That Scales Past Your Own Laptop

Checking Types With mypy in a Real Project

Install mypy, run it on a project, read its errors, and tighten the settings step by step. After this, you can add type checking to existing code without stopping all other work.

Course content

Sign in to save your progress.

Course Home

Type hints only catch bugs if something checks them. mypy is that something: it reads your code without running it and reports places where the types don't fit. The goal of this lesson is not a perfectly typed project on day one. It is a checker that runs on every change and gets a little stricter over time.

Skip ahead: If mypy already runs on your project with settings in pyproject.toml, and you know how to handle untyped libraries and # type: ignore, take the section quiz.

Is it worth it for your project?

For a 100-line script you run yourself, probably not. For code that several people change, that runs unattended, or that you will maintain for years, yes. The cost is a few minutes of setup and some hints. The payoff is that a whole category of bugs, especially the None nobody expected, gets reported while you write the code instead of when a customer hits it.

Install and run

Inside the project's virtual environment:

python -m pip install mypy

Here is a small module with two real bugs and one function without any hints:

# orders.py
def find_email(customers: dict[int, str], customer_id: int) -> str | None:
    return customers.get(customer_id)


def send_reminder(customers: dict[int, str], customer_id: int) -> None:
    email = find_email(customers, customer_id)
    print("sending to", email.lower())


def add_vat(amount: float) -> float:
    return round(amount * 1.12, 2)


def legacy_total(rows):
    return sum(r["amount"] for r in rows)


print(add_vat("100"))
python3 -m mypy orders.py
orders.py:8: error: Item "None" of "str | None" has no attribute "lower"  [union-attr]
orders.py:19: error: Argument 1 to "add_vat" has incompatible type "str"; expected "float"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Read each line as: file, line number, what is wrong, and the error code in square brackets. Line 8 calls .lower() on something that can be None. That crashes the first time a customer has no email. Line 19 passes a string where a number is expected. Neither error needed the code to run. The output above is from the mypy version current when this course was written; wording can change slightly between versions.

Notice what mypy didn't report: legacy_total. By default, mypy doesn't check inside functions without any hints. That is deliberate. It means you can add mypy to an old project without getting a thousand errors on the first day.

Fix the errors properly

# orders.py
def find_email(customers: dict[int, str], customer_id: int) -> str | None:
    return customers.get(customer_id)


def send_reminder(customers: dict[int, str], customer_id: int) -> None:
    email = find_email(customers, customer_id)
    if email is None:
        print(f"customer {customer_id} has no email, skipped")
        return
    print("sending to", email.lower())


def add_vat(amount: float) -> float:
    return round(amount * 1.12, 2)


def legacy_total(rows):
    return sum(r["amount"] for r in rows)


print(add_vat(float("100")))
python3 -m mypy orders.py
Success: no issues found in 1 source file

The None case is now handled on purpose, and the string is converted where it enters the program. That is the right fix. The wrong fix is the next section.

The escape hatches, and when not to use them

  • value.lower() # type: ignore[union-attr] silences one error on one line. Acceptable when you have checked that mypy is wrong, which is rare, and always with the error code in brackets so it doesn't hide other errors on that line.
  • typing.cast(str, value) tells mypy "trust me". It doesn't check or convert anything at runtime.
  • Any turns checking off for a value.

⚠ Warning: Silencing an error doesn't fix the bug behind it. email.lower() # type: ignore on the line from the example passes mypy and still crashes on a customer with no email. Before ignoring an error, be able to explain in one sentence why it can't happen. If you can't, it can.

Put the settings in pyproject.toml

So everyone on the team and the CI server run mypy the same way, keep the settings in the project:

[tool.mypy]
python_version = "3.12"
files = ["orders.py"]
check_untyped_defs = true
warn_unused_ignores = true
python3 -m mypy
Success: no issues found in 1 source file

With files set, plain mypy knows what to check. check_untyped_defs also looks inside functions without hints, like legacy_total, and reports obvious mistakes there. warn_unused_ignores reports # type: ignore comments that are no longer needed, so they don't pile up. (TOML isn't one of the code-block languages on this site, so it is labelled YAML here. The file is TOML.)

Tightening step by step

The strictest mode, strict = true, also requires hints on every function:

python3 -m mypy --strict orders.py
orders.py:18: error: Function is missing a type annotation  [no-untyped-def]
Found 1 error in 1 file (checked 1 source file)

On a big existing project, turning on strict at once gives you hundreds of errors and a team that hates mypy. A route that works better:

  1. Start with the defaults. Fix what it finds; those are usually real bugs.
  2. Add hints to new code and to every function you touch.
  3. Turn on check_untyped_defs.
  4. Turn on strict for one package at a time with a per-module section, for example [[tool.mypy.overrides]] with module = "shop.pricing" and strict = true.

Libraries without hints

Some installed packages ship without type information. mypy then reports something like "missing library stubs or py.typed marker". In order of preference:

  1. Check if a stubs package exists, usually named types-<package>, for example types-requests, and install it.
  2. If not, ignore that one library in the config with an override: module = "thatlibrary.*" and ignore_missing_imports = true.
  3. Don't set ignore_missing_imports = true globally. It also hides typos in your own import names.

See what mypy thinks

When an error makes no sense, ask mypy what type it inferred with reveal_type. It is only for the checker; remove it afterwards:

from orders import find_email

email = find_email({1: "ana@example.com"}, 1)
reveal_type(email)
python3 -m mypy check.py
check.py:4: note: Revealed type is "str | None"
Success: no issues found in 1 source file

Run it automatically

A checker that people have to remember to run doesn't get run. Add mypy next to pytest wherever your tests run automatically: a pre-commit hook, the CI pipeline, or at minimum a short script everybody uses before pushing. Tests check what the code does with specific inputs. mypy checks every path, including the ones nobody wrote a test for. They complement each other; neither replaces the other.

Common mistakes

  • Running mypy from a different environment. It needs to see the same installed packages as the project. Install it in the project's virtual environment.
  • Bare # type: ignore everywhere. Always add the error code, and turn on warn_unused_ignores.
  • Thinking "mypy passed" means "the code works". It means the types fit. The logic can still be wrong. That is what tests are for.

💡 Tip: Many editors run a type checker while you type (VS Code's Python extension uses Pylance, PyCharm has its own). Their results can differ slightly from mypy. Treat the editor as an early warning and the mypy run in CI as the one that decides.

What happens if you skip this step

You write type hints that nothing checks. They slowly drift away from what the code really does, and then they mislead people instead of helping them. Hints without a checker are comments. Comments go out of date.

If you ran mypy with default settings on your biggest project right now, how many of the errors do you think would turn out to be real bugs?

About

Install mypy, run it on a project, read its errors, and tighten the settings step by step. After this, you can add type checking to existing code without stopping all other work.

Duration
10 min
Last update
09/13/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.