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

bash
pip install mypy

Output: (none — exits 0 on success)

bash
uv add --dev mypy

Output: dependency added to the dev group in pyproject.toml

bash
poetry add --group dev mypy

Output: updated lockfile + dev install

bash
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 of 0.x releases).
  • 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/mypy GitHub 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

ExtraAdds
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):

bash
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

PackageTrade-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.
basedmypyA fork of mypy with stricter defaults. Use when stock mypy is too lax.

Common gotchas

  1. --strict is a meta-flag. It enables roughly ten individual rules (--disallow-untyped-defs, --disallow-incomplete-defs, --warn-return-any, --no-implicit-optional, …). Read mypy --help | grep strict to see the current bundle. Don't enable --strict everywhere day one — it floods CI.
  2. PEP 695 generic syntax only on 3.12+. Writing class Box[T]: ... requires --python-version 3.12 and a 3.12+ interpreter. On older targets, fall back to TypeVar.
  3. Stub packages don't always exist. pip install types- then <Tab> won't find every library. For unstubbed packages with no py.typed, mypy treats imports as Any — set [[tool.mypy.overrides]] with ignore_missing_imports = true to silence the warning.
  4. # type: ignore without an error code is too broad. Use # type: ignore[arg-type] so future errors of other types still surface. Enable warn_unused_ignores = true to catch dead # type: ignore lines.
  5. Cache lives in .mypy_cache/. Stale cache occasionally produces ghost errors after a major refactor or version bump. rm -rf .mypy_cache is the standard remedy.
  6. Any is contagious. A single Any-typed function return spreads through every downstream call site, silently disabling checks. Use --warn-return-any and --disallow-any-expr (via --strict) to catch this.
  7. 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:

toml
[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 = true globally; never relax. Cheap to maintain because every commit stays clean.
  • Existing untyped project, gradual. Start with strict = false and a tiny [[tool.mypy.overrides]] allowlist of modules with strict = true. Expand the allowlist module-by-module. Use ignore_errors = true for legacy code you'll touch later.
  • Library code with py.typed marker. Ship a py.typed empty 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.toml config at the root; mypy_path = "src/pkg1/src:src/pkg2/src" tells mypy where to find each sub-package. Run mypy from 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

toml
# 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

python
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

python
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

python
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]

python
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 with actions/cache@v4. Speed-up: 5–20× depending on change scope.
  • --sqlite-cache stores 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 with ignore_missing_imports = true for untyped third-party deps.
  • --no-incremental disables 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-grained enables 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 to TypeGuard.
  • Improved match statement narrowing on complex patterns.

mypy 1.10 (2024)

  • Pattern-matching narrowing improved; previously many patterns were inferred too loosely.
  • --enable-incomplete-feature=NewGenericSyntax for 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 corresponding types-* shim package is deprecated. mypy emits a warning; remove the shim.
  • Strictness defaults — new strict sub-flags are added periodically. A clean run on mypy 1.10 may emit new warnings on 1.13.
  • --python-version deprecation cycle — when Python EOL hits, that target version emits a deprecation warning for 2 minor releases, then is removed.

Upgrade pattern:

  1. Pin mypy + every types-* package exactly.
  2. Bump on a deliberate cadence. Read Changelog.md — the "Stricter checks" section is the relevant one.
  3. After the bump, run mypy --strict src/ once; fix or # type: ignore[new-code] each new error.
  4. 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:

PluginPurpose
pydantic.mypyUnderstands Pydantic's dynamic BaseModel.__init__ signature.
sqlalchemy[mypy] / sqlalchemy.ext.mypyModels declarative_base and Mapped[T] columns.
mypy_django_pluginDjango 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:

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

ErrorCauseFix
Missing return statement on a function with conditional returnsmypy can't prove every path returnsAdd 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 runtimeDynamic 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 stubInstall types-X, or add [[tool.mypy.overrides]] module = "X" ignore_missing_imports = true.
Incompatible return value type (got "...", expected "...") for a clearly-correct returnVariance 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 importUse string-form annotation "MyClass" or from __future__ import annotations.
# type: ignore ignored / not suppressingThe error class isn't covered by this comment's codeCheck mypy --show-error-codes; specify the right code.
mypy passes locally, fails in CIDifferent mypy version or types-* package versionsPin both exactly.
Cache-related ghost errors after major refactorStale .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: ignore lint 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-commitpre-commit/mirrors-mypy is the canonical hook source. Heavy; often gated to stages: [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

yaml
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 on pyproject.toml so 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_version config, not the runner version. No matrix needed.

For monorepos, parallelise per package:

yaml
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, pyright via 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: ignore and call it typed.

See also