Python Module 11 – Type hints and static type checking
mypy and pyright: running type checkers
Install mypy and pyright, run them on a project, configure both in pyproject.toml, read their errors and fix them instead of silencing them.
What you will learn
- Install and run mypy and pyright, and read each part of their messages
- Configure both checkers in pyproject.toml and explain where their defaults differ
- Fix an import that the checker cannot find without silencing it
- Use reveal_type and targeted ignore comments without hiding real bugs
- Plan a strict-mode rollout and run the checkers in CI
Before you start
What you’ll learn · Before you start
What you will learn
- Install and run mypy and pyright, and read each part of their messages
- Configure both checkers in pyproject.toml and explain where their defaults differ
- Fix an import that the checker cannot find without silencing it
- Use reveal_type and targeted ignore comments without hiding real bugs
- Plan a strict-mode rollout and run the checkers in CI
Before you start
On this page
A type checker: A program such as mypy or pyright that reads Python source without running it and reports every call, return and assignment that breaks a type hint, before the program runs. More in “Why type hints help (and what they don't do)” is a program you run on your code, the way you run tests, and on a file
with three bugs it reports all three before anything runs. Install
mypy and pyright into the project’s virtual environment with python -m pip install mypy pyright (pyright
also comes as an npm package and needs Node.js), then run mypy src or pyright src, or both with no arguments once
pyproject.toml names the files. Each prints one line per problem: the file, the line, the severity, the message and
a code that names the check. Configure them in pyproject.toml, under [tool.mypy] and [tool.pyright], and switch
on strict mode for new code. When a message is unclear, reveal_type(x) makes the checker print the type it inferred
for x; when a message is wrong for a good reason, an ignore comment silences that one error and nothing else, and
cast() tells the checker to treat a value as a given type without checking it.
Both checkers read the code and print one line per problem
The checkers here were installed as you would install them yourself, mypy with pip in a virtual environment and
pyright with npm, and each line that starts with $ is a command to type in the project’s folder. inventory.py has
three bugs:
from dataclasses import dataclass
@dataclass
class Item:
name: str
price: float
stock: int
def find(items: list[Item], name: str) -> Item | None:
for item in items:
if item.name == name:
return item
return None
def restock(item: Item, amount: int) -> None:
item.stock += amount
def value(items: list[Item]) -> float:
return sum(item.price * item.stock for item in items)
items = [Item("pen", 10.0, 120), Item("notebook", 45.5, "40")]
restock(find(items, "pen"), 30)
print(value(items).rjust(10)) from session import run
# Both checkers on the same file: the same three bugs, worded two ways.
run("""
mypy --version
pyright --version
mypy inventory.py
pyright inventory.py
""") Output
$ mypy --version mypy 2.4.0 (compiled: yes) $ pyright --version pyright 1.1.414 $ mypy inventory.py inventory.py:26: error: Argument 3 to "Item" has incompatible type "str"; expected "int" [arg-type] inventory.py:27: error: Argument 1 to "restock" has incompatible type "Item | None"; expected "Item" [arg-type] inventory.py:28: error: "float" has no attribute "rjust" [attr-defined] Found 3 errors in 1 file (checked 1 source file) [exit status 1] $ pyright inventory.py inventory.py inventory.py:26:57 - error: Argument of type "Literal['40']" cannot be assigned to parameter "stock" of type "int" in function "__init__" "Literal['40']" is not assignable to "int" (reportArgumentType) inventory.py:27:9 - error: Argument of type "Item | None" cannot be assigned to parameter "item" of type "Item" in function "restock" Type "Item | None" is not assignable to type "Item" "None" is not assignable to "Item" (reportArgumentType) inventory.py:28:20 - error: Cannot access attribute "rjust" for class "float" Attribute "rjust" is unknown (reportAttributeAccessIssue) 3 errors, 0 warnings, 0 informations [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_inventory.py
Both checkers found the same three bugs: a stock count given as the text "40", a find() result that may be
None passed where an Item is needed, and a method of str called on a float. mypy wrote one line per error;
pyright added the column and, for the second error, two lines that explain why None does not fit. Both ended with
a count and exited with status 1, which is what lets a CI job fail on type errors.
Read an error from the left: where it is, how serious it is, what is wrong, and which check said so. The diagram splits the line each checker printed for line 27 into those four parts:
The parts of an error message
Text description of the diagram
The diagram has two columns, one per checker, and splits the error that each one prints for line 27 of inventory.py into four parts, read from top to bottom.
- mypy: first the place, inventory.py:27, which is the file and the line; then the severity, error; then the message, Argument 1 to "restock" has incompatible type "Item | None"; expected "Item"; and last the error code in square brackets, [arg-type].
- pyright: first the place, inventory.py:27:9, which adds the column; then the severity, error; then the message, Argument of type "Item | None" cannot be assigned to parameter "item", followed on later lines by an explanation; and last the rule name in brackets, (reportArgumentType).
- The error code and the rule name are what configuration options and targeted ignore comments refer to.
The last part is the one to remember. mypy’s error code: The name of the check that a type checker's message comes from, such as mypy's [arg-type] or pyright's rule name reportArgumentType. Configuration options and targeted ignore comments refer to it., such as arg-type,
return-value or attr-defined, and pyright’s rule names, such as reportArgumentType, are what configuration
options and ignore comments refer to. When a message about an argument, a return value or an assignment lists two
types, the first is what the code has and the second is what the hint asked for; fix whichever of the two is wrong,
which is usually the code.
Checkpoint
Check: Running the checkers and reading a message
Both checkers read pyproject.toml, and their defaults differ
The two checkers follow one specification but are separate programs, with their own settings and defaults:
| Criterion | mypy | pyright |
|---|---|---|
| Written in | Python, compiled to C with mypyc | TypeScript, run by Node.js |
| Install | pip install mypy | pip install pyright, or npm install pyright |
| Settings | [tool.mypy] in pyproject.toml, or mypy.ini | [tool.pyright] in pyproject.toml, or pyrightconfig.json |
| Functions without hints | bodies skipped unless check_untyped_defs is on | bodies checked, with types inferred |
| Names of checks | error codes: [arg-type] | rule names: (reportArgumentType) |
| Strict mode | strict = true | typeCheckingMode = "strict" |
| When to choose | Projects that want fine-grained settings for each module | Fast checks and the same engine many editors use as you type |
mypy came first: PEP 484, which defined Python’s type hints, grew out of it. pyright is built for speed, and it
also runs inside editors as a
A program an editor runs in the background to check the code and complete names as you type. pyright works as one, so the errors an editor underlines can come from the same engine as a command-line run.,
which checks the code as you type. Both follow the typing specification, so they agree on most code, and they differ
in defaults and inference, as the previous lesson showed for a variable whose first value is 0.
One checker for CI
Choose one checker for CI, and treat the other’s messages as a second opinion when an error is hard to read.
A project’s pyproject.toml holds the settings of both checkers, each in its own section:
[project]
name = "shop"
version = "0.1.0"
requires-python = ">=3.14"
[tool.mypy]
files = ["src"]
strict = true
warn_unreachable = true
[tool.pyright]
include = ["src"]
typeCheckingMode = "strict" def reorder_level(daily_sales: list[int], lead_days: int) -> int:
"""Units to keep in stock: the average daily sale times the days a delivery takes."""
average = sum(daily_sales) / len(daily_sales)
return round(average * lead_days)
def needs_reorder(stock: int, daily_sales: list[int], lead_days: int = 3) -> bool:
return stock <= reorder_level(daily_sales, lead_days)
def label(name, stock):
return f"{name}: {stock} left" from session import run
# Run in the folder that holds pyproject.toml: both checkers read their settings and their files from it.
run("""
mypy
pyright
""") Output
$ mypy src/shop/stock.py:11: error: Function is missing a type annotation [no-untyped-def] Found 1 error in 1 file (checked 2 source files) [exit status 1] $ pyright src/shop/stock.py src/shop/stock.py:11:11 - error: Type of parameter "name" is unknown (reportUnknownParameterType) src/shop/stock.py:11:11 - error: Type annotation is missing for parameter "name" (reportMissingParameterType) src/shop/stock.py:11:17 - error: Type of parameter "stock" is unknown (reportUnknownParameterType) src/shop/stock.py:11:17 - error: Type annotation is missing for parameter "stock" (reportMissingParameterType) 4 errors, 0 warnings, 0 informations [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_project.py
Step through what each checker did in that folder:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- No file names. mypy is run in the project folder with no file names, the way a CI step runs it. Everything else it needs comes from the folder.
- Its own section. It finds pyproject.toml and reads its own section, [tool.mypy]. The [tool.pyright] section beside it is for the other checker.
- The files. files = ["src"] says what to check when no file is named. mypy checked 2 source files, src/shop/__init__.py and src/shop/stock.py, as the last line of its output says.
- The rules. strict = true turns on the strict checks at once, among them one that reports every function without hints.
- mypy's report. reorder_level() and needs_reorder() passed. label(name, stock) has no hints, so mypy reported it once, as [no-untyped-def]. One error makes the exit status 1, which fails a CI job.
- The same for pyright. pyright, run the same way, reads [tool.pyright] instead, include = ["src"] and typeCheckingMode = "strict". It reported label() four times: for each of its two parameters, once for the missing hint and once for the unknown type.
With no arguments, mypy checked the files listed under files and pyright those under include, and
strict mode: A setting that turns on a type checker's stricter checks at once, such as strict = true for mypy or typeCheckingMode = "strict" for pyright. Among other things it reports every function without hints. reported the one function without hints, label(), while the two
annotated functions passed. Strict mode is the right default for new code; existing code is moved to it in steps.
Checkpoint
Check: Settings and defaults of the two checkers
A checker must see the same packages as the program
A checker reads the modules your code imports, so it must run with the same packages installed as the program. Run it inside the project’s virtual environment, and it finds them there:
import yaml
from shop_utils import slugify
settings = yaml.safe_load("currency: INR")
print(slugify(settings["currency"])) from session import run
# PyYAML and shop_utils are not installed in this environment.
run("""
mypy imports.py
pyright imports.py
""") Output
$ mypy imports.py imports.py:1: error: Library stubs not installed for "yaml" [import-untyped] imports.py:1: note: Hint: "python3 -m pip install types-PyYAML" imports.py:1: note: (or run "mypy --install-types" to install all missing stub packages) imports.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports imports.py:2: error: Cannot find implementation or library stub for module named "shop_utils" [import-not-found] Found 2 errors in 1 file (checked 1 source file) [exit status 1] $ pyright imports.py imports.py imports.py:1:8 - warning: Import "yaml" could not be resolved from source (reportMissingModuleSource) imports.py:2:6 - error: Import "shop_utils" could not be resolved (reportMissingImports) 1 error, 1 warning, 0 informations [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_imports.py
The two messages mean different things. shop_utils is missing altogether, so every name taken from it is unknown:
install the package, or fix the import. yaml is PyYAML, a library that ships without hints. mypy names the
separate stub package: A package that adds type hints for a library that ships without them, such as types-PyYAML for PyYAML. It is installed beside the type checkers, as a development dependency. that adds them, types-PyYAML, while pyright found those stubs
among the ones it bundles and only warned that the library itself is not installed here. Stub packages are ordinary
development dependencies: list them beside the checkers, so that CI and every laptop check the same way.
Checkpoint
Check: Imports the checker cannot find
Check yourself
Practice that adapts to you
Practise this lesson
9 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Fixing errors without silencing them: reveal_type, casts and ignores Pro: opens with a Pro, Premium or Ultimate pass
An error message is a question about your code, and silencing it only hides the question. This part traces types
with reveal_type, reproduces what each way of quieting an error really does, and finishes with an exercise that
fixes nine errors without hiding any.
In this part:
- Tracing a narrowing line by line, with the type each checker reveals
- Five ways to answer an error, and what each one really does
- Fixing nine errors, as an exercise
- Fix nine type errors without silencing any of them
5 runnable examples (Python) · 1 exercise with sample tests · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Strict mode, a module-by-module rollout and CI, measured Pro: opens with a Pro, Premium or Ultimate pass
Strict mode finds more bugs and demands more hints, and the two checkers’ strict modes are not the same size. This part measures them on one module, reproduces a configuration mistake that makes strictness leak into untouched code, and sets up the checks a CI job runs.
In this part:
- What strict mode adds, measured on one module
- Rolling strictness out module by module, and a setting that leaks
- Running the checkers in CI
1 diagram · 7 runnable examples (Python, TOML, Text) · about 8 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Mid to senior
CI runs mypy and passes, but half the team uses an editor whose pyright-based checker shows errors in the same files, and people have started adding ignore comments to quiet their editors. How do you resolve it?
-
Senior to staff
A code base has 1,400 "# type: ignore" comments, most of them without an error code. How do you reduce them safely, and how do you stop new ones from piling up?
-
Senior to staff
Plan strict type checking for a 300-module repository shared by six teams, where the type check already takes 4 minutes in CI. In what order do you go, how do you configure it, and what do you measure?
Key takeaways
- Install mypy and pyright in the project’s environment and run them like tests: they read the code and run nothing.
- Each error names a place, a severity, a message and a code; the code is what settings and ignores refer to.
- Both checkers read
pyproject.tomland share one specification, but their defaults differ, so CI runs one of them. - A checker must see the program’s packages, and the stub packages of libraries that ship without hints.
Terms in this lesson
- error code
- The name of the check that a type checker's message comes from, such as mypy's [arg-type] or pyright's rule name reportArgumentType. Configuration options and targeted ignore comments refer to it.
- strict mode
- A setting that turns on a type checker's stricter checks at once, such as strict = true for mypy or typeCheckingMode = "strict" for pyright. Among other things it reports every function without hints.
- stub package
- A package that adds type hints for a library that ships without them, such as types-PyYAML for PyYAML. It is installed beside the type checkers, as a development dependency.
References
- PEP 484: Type Hints (Python Software Foundation)
- mypy documentation (The mypy project)
- mypy documentation: the mypy command line (The mypy project)
- mypy documentation: the mypy configuration file (The mypy project)
- mypy documentation: error codes (The mypy project)
- mypy documentation: using mypy with an existing codebase (The mypy project)
- mypy documentation: changelog (defaults changed in mypy 2.0) (The mypy project)
- mypy source code: main.py, where mypy sets its exit status (The mypy project)
- Pyright documentation: configuration (Microsoft)
- Pyright documentation: command-line interface and exit codes (Microsoft)
- Pyright documentation: comments (Microsoft)
- Pyright documentation: installation (Microsoft)
- typing.reveal_type() (Python Software Foundation)
- typing.cast() (Python Software Foundation)
- Writing your pyproject.toml (Python Packaging Authority)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress