skip to content

mypy — Reference Static Type Checker for Python

Package-level reference for mypy on PyPI — install variants, Python compat, the types-* stub-package ecosystem, and alternatives.

13 min read 14 snippets deep dive

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

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

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:

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

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

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

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:

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#