mypy#
What it is#
mypy is the reference implementation of PEP 484 static type checking for Python, originally written by Jukka Lehtosalo and maintained by a community core team with sponsorship from Dropbox (the largest historical user). It reads type annotations and reports type errors without running the program — wrong argument types, missing returns, None-dereferences, incompatible overrides.
It’s a gradual type checker: it tolerates un-annotated code and only checks what you tell it to. --strict flips that default and demands annotations everywhere.
Install#
pip install mypy
Output: (none — exits 0 on success)
uv add --dev mypy
Output: dependency added to the dev group in pyproject.toml
poetry add --group dev mypy
Output: updated lockfile + dev install
pipx install mypy
Output: installed to isolated venv, mypy CLI on PATH. Caveat: mypy needs to import your project’s dependencies to type-check them — pipx-installed mypy can’t see your project venv, so you usually want mypy inside the project venv, not as a pipx-global.
Versioning & Python support#
- Current line is
1.x(mypy hit 1.0 in 2023 after years of0.xreleases). - Minor-version cadence is roughly every 2–3 months. Releases can introduce stricter checks — pin in CI and bump deliberately.
- Recent releases run on Python 3.8+ and can check code targeting any version via
--python-version 3.7..3.12. The runtime and the check target are independent. - PEP 695 (new generic syntax in Python 3.12) is supported but only on the matching
--python-version. - Mypy follows roughly semver — major-version bumps signal genuine breakage in the public API.
Package metadata#
- Maintainer:
python/mypyGitHub org (community core team) - Project home: github.com/python/mypy
- Docs: mypy.readthedocs.io
- PyPI: pypi.org/project/mypy
- License: MIT
- Governance: community core team; Dropbox historically the primary corporate sponsor
- First released: 2012
- Downloads: tens of millions per month
Optional dependencies & extras#
| Extra | Adds |
|---|---|
mypy[install-types] | Adds the --install-types runtime helper that auto-installs missing types-* stub packages on demand. |
mypy[reports] | lxml for HTML/XML coverage reports. |
mypy[mypyc] | The mypyc compiler that ships in the same source tree (compile typed Python to C). Different tool than the type checker. |
mypy[faster-cache] | Uses orjson for faster cache I/O. |
mypy bundles typeshed — the third-party repository of type stubs for the stdlib and many popular packages. For packages not in typeshed, install the per-package types-* stub package (e.g. types-requests, types-PyYAML):
pip install types-requests types-PyYAML
Output: stub packages installed; mypy now type-checks import requests. Not all packages have stubs — projects that ship py.typed markers don’t need them, and obscure libraries simply aren’t covered.
Core deps: typing-extensions, mypy-extensions, tomli (on Python < 3.11).
Alternatives#
| Package | Trade-off |
|---|---|
pyright (Microsoft) | Faster, written in TypeScript, powers Pylance in VS Code. Stricter inference; different error messages. The de-facto standard for editor-integrated type checking. |
pyre (Meta) | Built for huge codebases (the Instagram/Facebook scale). OCaml-based. Sparse documentation outside Meta’s use cases. |
pytype (Google) | Lattice-based inference — types from untyped code. Slower; runs only on Linux/macOS. Niche. |
ty (Astral) | Astral’s in-development Rust type checker. Promises mypy-compatible behaviour at ruff-like speed. Watch this space. |
basedmypy | A fork of mypy with stricter defaults. Use when stock mypy is too lax. |
Common gotchas#
--strictis a meta-flag. It enables roughly ten individual rules (--disallow-untyped-defs,--disallow-incomplete-defs,--warn-return-any,--no-implicit-optional, …). Readmypy --help | grep strictto see the current bundle. Don’t enable--stricteverywhere day one — it floods CI.- PEP 695 generic syntax only on 3.12+. Writing
class Box[T]: ...requires--python-version 3.12and a 3.12+ interpreter. On older targets, fall back toTypeVar. - Stub packages don’t always exist.
pip install types-then<Tab>won’t find every library. For unstubbed packages with nopy.typed, mypy treats imports asAny— set[[tool.mypy.overrides]]withignore_missing_imports = trueto silence the warning. # type: ignorewithout an error code is too broad. Use# type: ignore[arg-type]so future errors of other types still surface. Enablewarn_unused_ignores = trueto catch dead# type: ignorelines.- Cache lives in
.mypy_cache/. Stale cache occasionally produces ghost errors after a major refactor or version bump.rm -rf .mypy_cacheis the standard remedy. Anyis contagious. A singleAny-typed function return spreads through every downstream call site, silently disabling checks. Use--warn-return-anyand--disallow-any-expr(via--strict) to catch this.- mypy needs to import your code’s deps. Unlike a pure-syntactic linter, it actually executes module-level code during analysis. Side-effecting top-level code in dependencies can break type-checking in surprising ways — keep imports lazy.
Configuration & layout patterns#
mypy’s config lives in pyproject.toml under [tool.mypy] (modern) or mypy.ini / setup.cfg (legacy — supported but not canonical). Per-module overrides use [[tool.mypy.overrides]] arrays:
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true
warn_redundant_casts = true
show_error_codes = true
show_column_numbers = true
pretty = true
exclude = ['migrations/', 'generated/']
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = ["legacy_lib.*", "untyped_third_party"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "mypkg.legacy.*"
ignore_errors = true
Strategies by codebase shape:
- New project, strict from day one.
strict = trueglobally; never relax. Cheap to maintain because every commit stays clean. - Existing untyped project, gradual. Start with
strict = falseand a tiny[[tool.mypy.overrides]]allowlist of modules withstrict = true. Expand the allowlist module-by-module. Useignore_errors = truefor legacy code you’ll touch later. - Library code with
py.typedmarker. Ship apy.typedempty file in the package; mypy then trusts the in-tree type info for downstream users. Without this, your library’s types are invisible. - Monorepo. Single
pyproject.tomlconfig at the root;mypy_path = "src/pkg1/src:src/pkg2/src"tells mypy where to find each sub-package. Runmypyfrom the repo root with explicit module names:mypy -p mypkg.
The mypy_path setting is the most common monorepo gotcha — without it, mypy can’t find packages that aren’t installed. The src/ layout requires either pip install -e . per sub-package or explicit mypy_path.
Real-world recipes#
Gradual typing on a 100k-line codebase#
# pyproject.toml — phase 1: enable mypy without exploding CI
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
follow_imports = "silent"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "mypkg.core.*"
strict = true
Run mypy in CI on the core module only at first. Once green, add mypkg.api.*, then mypkg.workers.*. Six months in, flip the top-level strict = true and adjust the remaining overrides.
The --strict toggle is a meta-flag expanding to ~10 individual rules. The modern strict set includes: warn_unused_configs, disallow_any_generics, disallow_subclassing_any, disallow_untyped_calls, disallow_untyped_defs, disallow_incomplete_defs, check_untyped_defs, disallow_untyped_decorators, no_implicit_optional, warn_redundant_casts, warn_unused_ignores, warn_return_any, no_implicit_reexport, strict_equality.
Type-narrowing a Union#
from typing import TypeGuard
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_str_list(items):
# mypy now treats items as list[str] inside this block
print(", ".join(items))
TypeGuard (PEP 647) lets mypy narrow a Union based on a custom predicate. The newer PEP 742 TypeIs (Python 3.13+) is stricter — recommended for new code if you can target 3.13.
Protocol for duck-typed APIs#
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
def shutdown(resource: SupportsClose) -> None:
resource.close()
Protocol types are structural — any class with a matching close() method satisfies SupportsClose without inheriting. This is the typed-Python equivalent of duck typing.
TypedDict for JSON shapes#
from typing import TypedDict, NotRequired
class UserPayload(TypedDict):
id: int
name: str
email: NotRequired[str] # optional field
def update_user(payload: UserPayload) -> None:
...
mypy enforces both the presence of required fields and the absence of unknown ones. total=False (class-level) makes all fields optional; NotRequired (PEP 655) is per-field, more granular.
Per-module # type: ignore[code]#
import legacy_module # type: ignore[import-not-found]
result: int = legacy_module.compute() # type: ignore[no-any-return]
Always include the error code — # type: ignore without one silences everything in that line, hiding new errors. warn_unused_ignores = true flags ignores that no longer suppress anything (i.e. the underlying issue is now fixed).
Performance tuning#
mypy is famously the slow link in any Python CI pipeline. Levers, in impact order:
dmypy(daemon mode). Long-running mypy process that incrementally re-checks changed files. ~10× faster on warm cache.dmypy start,dmypy check src/,dmypy stop. Editor integrations (PyCharm, VS Code via Pylance with--useTypingExtensions) use the daemon transparently.- Cache lives in
.mypy_cache/. Per-Python-version, content-hashed. Add to.gitignore; cache in CI withactions/cache@v4. Speed-up: 5–20× depending on change scope. --sqlite-cachestores the cache in SQLite instead of many small files. Faster on Windows (small-file I/O is slow) and network filesystems.follow_imports = "silent"stops mypy from analysing dependencies’ source. Trades coverage for speed. Pair withignore_missing_imports = truefor untyped third-party deps.--no-incrementaldisables the cache. Useful only for debugging spurious behaviour — never in normal use.- Parallel by module — mypy itself is not parallel within a run, but you can split CI:
mypy -p mypkg.core &; mypy -p mypkg.api &; wait. Effective up to 4 splits. --cache-fine-grainedenables daemon-level cache stability across restarts. Experimental but solid.
For 1M+-line codebases, the daemon plus aggressive caching is the difference between a 30-second incremental check and a 5-minute full re-analysis.
Version migration guide#
mypy’s major-version bumps are conservative; minor bumps regularly tighten checks. Recent inflection points:
mypy 1.13 (2024)
TypeIs(PEP 742) supported as a stricter alternative toTypeGuard.- Improved
matchstatement narrowing on complex patterns.
mypy 1.10 (2024)
- Pattern-matching narrowing improved; previously many patterns were inferred too loosely.
--enable-incomplete-feature=NewGenericSyntaxfor PEP 695 became default-on for 3.12 target.
mypy 1.0 (2023)
- Long-anticipated 0.x → 1.0 bump. Mostly signalled stability rather than breakage. Some long-deprecated flags removed.
Recurring patterns across releases:
- Stub package shims — when an upstream library adds
py.typed, the correspondingtypes-*shim package is deprecated. mypy emits a warning; remove the shim. - Strictness defaults — new
strictsub-flags are added periodically. A clean run on mypy 1.10 may emit new warnings on 1.13. --python-versiondeprecation cycle — when Python EOL hits, that target version emits a deprecation warning for 2 minor releases, then is removed.
Upgrade pattern:
- Pin mypy + every
types-*package exactly. - Bump on a deliberate cadence. Read
Changelog.md— the “Stricter checks” section is the relevant one. - After the bump, run
mypy --strict src/once; fix or# type: ignore[new-code]each new error. - If a stub package is now redundant, remove from dev deps.
Plugin & rule ecosystem#
mypy supports plugins — Python modules that hook into the type checker to add custom inference rules. Used by:
| Plugin | Purpose |
|---|---|
pydantic.mypy | Understands Pydantic’s dynamic BaseModel.__init__ signature. |
sqlalchemy[mypy] / sqlalchemy.ext.mypy | Models declarative_base and Mapped[T] columns. |
mypy_django_plugin | Django ORM model fields, manager methods, settings. |
attrs.mypy | @attrs.define field inference. |
numpy.typing (not strictly a plugin) | Provides NDArray[np.float64] etc. |
Enable in pyproject.toml:
[tool.mypy]
plugins = ["pydantic.mypy", "mypy_django_plugin.main"]
[tool.django-stubs]
django_settings_module = "myproj.settings"
Plugins are tied to mypy major versions; bump them together.
Troubleshooting common errors#
| Error | Cause | Fix |
|---|---|---|
Missing return statement on a function with conditional returns | mypy can’t prove every path returns | Add an explicit raise or return None to the missing branch; or annotate -> NoReturn if the function never returns. |
Module has no attribute X despite the attribute existing at runtime | Dynamic attribute (e.g. via __getattr__) | Add a stub method or use cast(Any, obj).X. |
Cannot find implementation or library stub for module 'X' | No py.typed marker and no types-X stub | Install types-X, or add [[tool.mypy.overrides]] module = "X" ignore_missing_imports = true. |
Incompatible return value type (got "...", expected "...") for a clearly-correct return | Variance issue (Liskov, contravariance) | Restructure with Protocol, or use TypeVar with bound=. |
Argument of type "Optional[X]" is not assignable to parameter of type "X" | Implicit Optional disabled (default since 0.990) | Guard with if x is not None: or annotate as Optional[X] explicitly. |
Cannot determine type of "..." | Forward reference or circular import | Use string-form annotation "MyClass" or from __future__ import annotations. |
# type: ignore ignored / not suppressing | The error class isn’t covered by this comment’s code | Check mypy --show-error-codes; specify the right code. |
| mypy passes locally, fails in CI | Different mypy version or types-* package versions | Pin both exactly. |
| Cache-related ghost errors after major refactor | Stale .mypy_cache/ | rm -rf .mypy_cache && mypy .... |
mypy --show-error-codes --show-error-context is the surgical flag combo for unclear errors — it includes the error code (so you can # type: ignore[<code>]) and surrounding code.
Ecosystem integrations#
mypy sits alongside, not inside, most other tools:
pyright(Microsoft) — competitor type checker. Powers Pylance in VS Code. Faster (TypeScript-native, multithreaded). Different inference algorithm — pyright is generally stricter and faster. Some projects run both; pyright in editors, mypy in CI.ty(Astral) — in-development Rust type checker, mypy-compatible target. Promises ruff-like speed. Watch this space.basedmypy— fork of mypy with stricter defaults and additional# type: ignorelint rules.pyre(Meta) — built for huge codebases. OCaml-based; sparse public docs.pytype(Google) — lattice-based inference; types from untyped code. Slower; Linux/macOS only.pre-commit—pre-commit/mirrors-mypyis the canonical hook source. Heavy; often gated tostages: [push]instead of[commit].dmypy— daemon mode for incremental editor checks.mypyc— compiles type-annotated Python to C. Different tool; shares the source repo. Black uses mypyc to compile itself.
CI integration#
name: types
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/cache@v4
with:
path: .mypy_cache
key: mypy-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
- run: pip install -e ".[type-check]"
- run: mypy src/ tests/
Key choices:
- Cache
.mypy_cache/— biggest single CI speedup. Hash onpyproject.tomlso dep changes invalidate. - Install the project (
pip install -e .) — mypy imports your code to type-check it. Stub-only checks miss runtime-side type info. - Pin mypy version in the extras group:
[project.optional-dependencies] type-check = ["mypy==1.13.0", "types-requests"]. - Single Python version — mypy’s behaviour depends on
python_versionconfig, not the runner version. No matrix needed.
For monorepos, parallelise per package:
strategy:
matrix:
package: [api, core, workers]
steps:
- run: mypy -p ${{ matrix.package }}
Each cell has its own .mypy_cache/. Net wall-clock time drops to the slowest cell.
When NOT to use this#
- Tiny scripts — a 100-line script doesn’t need static type checking. The ratio of annotation effort to bug catch is poor.
- Highly dynamic libraries — code that uses
__getattr__,eval, dynamic class creation extensively. mypy hits its limits here; pyright sometimes does better. - Codebases that mix typed Python with heavy C extensions without stubs. The unstubbed C boundary throws away type info; the value of mypy upstream of that boundary is reduced.
- Editor-only typing. If you only want IDE hints,
pyrightvia Pylance is faster and editor-native. mypy’s value is CI-enforced gradual typing. - Pre-PEP 484 codebases that can never afford a typing pass. Gradual is possible but the discipline is real — without commitment, you accumulate
# type: ignoreand call it typed.
See also#
- Python: mypy — strict-mode configuration, error codes,
# type: ignorepatterns - Concept: API — type-checking as contract enforcement
- Packages: pip-ruff — lint alongside type-check
- Packages: pip-pre-commit — run mypy on every commit