Checking Code with mypy
20 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Hints only help when something reads them. mypy reads your hints and reports mistakes before the program runs. Run it in your terminal, in the same environment as your project.
Install mypy in your environment
mypy is a tool that reads your type hints and reports mismatches without running the program. Each message gives the file, the line, the problem and an error code.
mypy is a separate tool. Install it inside the virtual environment of your project, like any other package. It is used while developing, so it does not go into the program's own dependencies.
python -m pip install mypymypy --versionCollecting mypy Downloading mypy-2.4.1-cp313-cp313-win_amd64.whl (11.4 MB) ... Installing collected packages: typing_extensions, pathspec, mypy_extensions, librt, ast-serialize, mypy Successfully installed ast-serialize-0.13.1 librt-0.16.0 mypy-2.4.1 mypy_extensions-1.2.0 pathspec-1.1.1 typing_extensions-4.16.0 mypy 2.4.1 (compiled: yes)
The exact versions change over time. python -m mypy runs the same tool if the mypy command is not found.
Run it on a file
This file runs without any error, because Python does not check hints. Both marked lines break the hints. Save it as checkout.py and point mypy at it.
def total_price(price: float, quantity: int) -> float: return price * quantity
print(total_price("100", 3))label: str = total_price(10.0, 2)Example 1 · Python runs it without complaint
def total_price(price: float, quantity: int) -> float: return price * quantity
print(total_price("100", 3))label: str = total_price(10.0, 2)100100100
What this code does
- Python ignores the hints, so
"100" * 3quietly gives the wrong answer. - There is no error, only a surprising price. This is the kind of bug mypy finds.
Example 2 · mypy on the same file
Runs in: Your terminal
mypy checkout.pycheckout.py:4: error: Argument 1 to "total_price" has incompatible type "str"; expected "float" [arg-type] checkout.py:5: error: Incompatible types in assignment (expression has type "float", variable has type "str") [assignment] Found 2 errors in 1 file (checked 1 source file)
What this code does
- Line 4 passes a string where a float is expected.
- Line 5 puts a float into a name hinted
str. The code in brackets names the kind of error.
The first example below runs the file. The second runs mypy on it.
Reading a message
Every message has the same parts: file, line number, error:, the problem in words, and the error code in square brackets. Here the lookup can return None.
def find_user(user_id: int) -> str | None: users = {1: "Priya", 2: "Arjun"} return users.get(user_id)
name = find_user(3)print(name.upper())Example 3 · A possible None
Runs in: Your terminal
mypy users.pyusers.py:6: error: Item "None" of "str | None" has no attribute "upper" [union-attr] Found 1 error in 1 file (checked 1 source file)
What this code does
- mypy knows
find_usercan returnNone, soname.upper()is not safe. - Wrapping the call in
if name is not None:removes the message.
Run mypy users.py. The message names the exact type mypy worked out, str | None, and the call that is not allowed on None. The code union-attr is what you would put in an ignore comment.
Untyped code and strict mode
Strict mode is the mypy flag --strict. It reports functions without hints and switches on the stricter checks, so untyped code can no longer hide mistakes.
By default mypy skips the body of a function that has no hints. A function with no hints passes, however wrong it is. The --strict flag asks for hints everywhere and turns on more checks.
def add_tax(amount, rate): return amount + "tax"Example 4 · Untyped code passes, strict does not
Runs in: Your terminal
mypy shop.pymypy --strict shop.pySuccess: no issues found in 1 source file shop.py:1: error: Function is missing a type annotation [no-untyped-def] Found 1 error in 1 file (checked 1 source file)
What this code does
- Without hints, the body is skipped, so the first run says "Success".
- With
--strict, mypy asks for the hints. The bad body is still not reported until the function has hints.
Example 5 · Hints turn the body check on
Runs in: Your terminal
mypy shop.pyshop.py:2: error: Unsupported operand types for + ("float" and "str") [operator]
Found 1 error in 1 file (checked 1 source file)What this code does
- The function now reads
def add_tax(amount: float, rate: float) -> float, so mypy looks inside. float + stris reported as anoperatorerror.
Add the hints and mypy checks the body at once. Bring a codebase to strict one file at a time, as you add hints.
Narrowing, reveal_type and ignore
Type narrowing is how a checker refines a wide type after a test such as isinstance or is not None. Inside that branch it knows the smaller type.
To see what mypy believes about a value, add reveal_type(x). It prints the type at that line and is removed after use. To fix a None warning, check the value first.
def to_amount(value: int | str) -> int: reveal_type(value) if isinstance(value, str): reveal_type(value) return int(value) return valueExample 6 · After checking for None
Runs in: Your terminal
mypy users.pySuccess: no issues found in 1 source file
What this code does
- With the
is not Nonetest,nameis astrinside the branch. - "Success" means no problem was found in the files checked. It does not prove the code is correct.
Example 7 · reveal_type output
Runs in: Your terminal
mypy narrow.pynarrow.py:2: note: Revealed type is "int | str" narrow.py:4: note: Revealed type is "str" Success: no issues found in 1 source file
What this code does
- The first line shows the declared union. The second shows the narrowed
str. - Remove
reveal_typecalls before running the program for real.
The type changes inside the if: int | str becomes str. As a last resort, # type: ignore[code] on a line silences one specific error. Give the code, so other errors on that line still show.
Next step
Check your understanding
11 quick questions