cheat sheet
mypy
Package-level reference for mypy on PyPI — install variants, Python compat, the types-* stub-package ecosystem, and alternatives.
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