mypy
mypy is the original static type checker for Python and the reference implementation of PEP 484 type hints. You annotate your code, run mypy over it, and it reports type errors without executing anything: passing a str where an int is expected, forgetting a None check, breaking a Protocol. It is built for gradual typing, so you can annotate one module at a time and leave the rest dynamic. It ships with a daemon (dmypy) for fast incremental re-checks on large codebases, error codes you can suppress individually, and it is itself compiled with mypyc, which the project says makes it about 4x faster than running interpreted. It lives under the python GitHub org and effectively defines how Python typing behaves in practice.
Still the reference type checker and the safest bet for CI and library authors, with the deepest plugin and config surface. If your main use is editor feedback or you refuse daemon setup, pyright will feel faster; many serious teams simply run both.
Use it if
- You want the checker the Python typing ecosystem treats as the baseline; most libraries test their stubs and typing behavior against mypy first
- You need CI-grade type checking with fine-grained control: per-module strictness, error codes, and a plugin system that packages like Django (via django-stubs) and SQLAlchemy rely on
- You have a large codebase and will use dmypy; the daemon's incremental checks turn multi-minute runs into sub-second ones
- You are adopting types gradually and want a tool designed for half-typed codebases rather than one that assumes full coverage
- You mainly want in-editor feedback; pyright powers VS Code's Pylance, is much faster on cold runs, and often infers more without annotations, while mypy's editor integrations are thinner
- A cold full run on a big monorepo has to be fast without daemon setup; plain mypy is slow at scale and dmypy is a workflow you have to adopt, not a default
- Your team will not maintain a config; out of the box mypy is permissive enough to miss a lot (untyped defs are unchecked by default), and a useful setup means curating strictness flags and per-module overrides
- You cannot run Python 3.10+ tooling; mypy 2.x requires Python 3.10 or newer to run, so pinned older toolchains are stuck on the 1.x line
Setup reality
pip install mypy and run mypy src/, that part is genuinely easy. The work starts on real codebases: the first run on untyped code produces a wall of errors, mostly from third-party packages without stubs, so you immediately learn --install-types and per-module ignore_missing_imports overrides. Defaults are lenient (functions without annotations are not checked), so teams end up assembling a [tool.mypy] block in pyproject.toml with strict flags, excludes for generated code, and overrides per legacy package. Caches in .mypy_cache occasionally go stale after big refactors and need deleting. The dmypy daemon is a separate habit with its own quirks: it can crash after certain edits and needs a dmypy restart. Plugins (django-stubs, pydantic) each bring their own config stanza.
Patterns
Install and check a projectfirst-run
python3 -m pip install -U mypy
mypy src/mypy 2.x needs Python 3.10+ to run; functions without any annotations are skipped by default, so a clean first run does not mean much yet.
Configure via pyproject.tomlpyproject-config
[tool.mypy]
python_version = "3.12"
files = ["src"]
warn_unused_ignores = true
warn_redundant_casts = true
disallow_untyped_defs = trueWithout a files/packages setting, what gets checked depends on how you invoke mypy, which makes CI and local runs drift apart.
Turn on strict checkingstrict-mode
mypy --strict src/
# or in pyproject.toml
[tool.mypy]
strict = truestrict is a bundle of individual flags; run mypy --help to see what it enables, then relax specific flags per module instead of abandoning it.
Silence untyped third-party importsmissing-stubs
[[tool.mypy.overrides]]
module = ["somepkg.*", "legacy_sdk"]
ignore_missing_imports = trueScope the override to specific modules; a global ignore_missing_imports = true also hides your own typos in import paths.
Install missing stub packagesinstall-types
mypy --install-types --non-interactive src/This pip-installs types-* stubs for imports mypy recognizes; in CI prefer pinning stub packages in requirements so builds stay reproducible.
Suppress one error with its codetargeted-ignore
value: int = get_legacy_value() # type: ignore[assignment]Always include the error code in brackets; a bare # type: ignore hides every future error on that line, and warn_unused_ignores flags stale ones.
Debug what mypy thinks a type isreveal-type
x = load_config()
reveal_type(x) # note: Revealed type is "dict[str, Any]"reveal_type needs no import for mypy runs, but it crashes at runtime under plain Python, so delete it before committing (or import it from typing_extensions).
Fast incremental checks with dmypydaemon-mode
dmypy run -- src/
# subsequent runs are incremental and usually sub-second
dmypy restart # when results look stale after big refactorsThe daemon keeps state between runs; after switching branches or upgrading mypy, restart it before trusting the output.
Narrow Optional and union typesnarrowing
def send(user: User | None) -> None:
if user is None:
raise ValueError('no user')
# user is User from here on
user.notify()
def handle(event: Click | Scroll) -> None:
if isinstance(event, Click):
event.press()mypy narrows on is None, isinstance, and assert; stashing the check result in a variable first can defeat narrowing.
Strict for new code, lenient for legacygradual-adoption
[tool.mypy]
disallow_untyped_defs = true
[[tool.mypy.overrides]]
module = "legacy.*"
disallow_untyped_defs = false
ignore_errors = trueRatchet one package at a time out of the legacy override list; this beats a repo-wide strictness that everyone ignores.
Exclude generated or vendored codeexclude-paths
[tool.mypy]
exclude = [
"^build/",
"_pb2\\.py$",
"^migrations/"
]exclude entries are regexes, not globs; an excluded file still gets checked if another checked module imports it, use per-module ignore_errors for that.
Run mypy in pre-commitpre-commit-hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.3.0
hooks:
- id: mypy
additional_dependencies: [types-requests, pydantic]The mirror runs mypy in an isolated venv, so it cannot see your project's dependencies unless you list them in additional_dependencies; many teams run mypy in CI instead for this reason.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pyright | PyPI | You want the fastest checks and the same engine as VS Code's Pylance, configured via pyrightconfig or pyproject |
| basedpyright | PyPI | You want pyright's engine fully on PyPI with the Pylance-only features reimplemented and no Node dependency |
| ty | PyPI | You want to try Astral's Rust-based checker for speed and are comfortable with a young tool |
| pyre-check | PyPI | Meta-scale monorepos where Pyre's incremental architecture and Pysa taint analysis matter |