Skip to content

mypy - standard typechecker

Mental Model

mypy is best understood as:

  • A static analyzer that reads your code + annotations and tries to prove consistency
  • A gradual typing tool: you can adopt it incrementally, and config is how you control that gradient
  • A policy engine: strictness is a set of knobs that encode how much ambiguity you tolerate

Key idea:

  • The hard part is not “adding annotations”
  • The hard part is choosing boundaries (what is checked) and strictness (how aggressively)

Official docs: configuration overview and flag semantics are documented in the mypy config docs.

Configuration Locations & Precedence

mypy supports configuration via:

  • mypy.ini
  • setup.cfg
  • pyproject.toml

The “Strictness Surface”

strict = True is a bundle, not a single behavior

strict enables a collection of optional error checking flags. The exact bundle can evolve over time, so treat it as a moving preset rather than a stable contract.

If you need stability, you can:

  • Use strict = True for the general posture
  • Override individual flags explicitly as needed

What strict roughly includes (conceptually)

Expect strict posture to push on:

  • Disallowing untyped defs / calls
  • Disallowing implicit Optional
  • Warning on unused ignores / redundant casts
  • Stricter equality and re-exports

Baseline Config (Practical, Not Maximal)

This is a common “serious but survivable” starting point:

[tool.mypy]
python_version = "3.11"
warn_unused_configs = true
# Gradual but disciplined
check_untyped_defs = true
disallow_incomplete_defs = true
# Keeps Optional honest
no_implicit_optional = true
# Hygiene
warn_unused_ignores = true
warn_redundant_casts = true
no_implicit_reexport = true
# Boundary control
ignore_missing_imports = false

Why this shape:

  • You get meaningful guarantees without immediately requiring annotation coverage everywhere
  • You keep the most common “gradual typing footguns” under control (especially Optional)

Per-Module Configuration (How You Scale Adoption)

The real power move in mypy isn’t global flags — it’s per-module policy.

Typical pattern:

  • Strict in app/ (your code)
  • Looser in tests/
  • Very specific carve-outs for legacy or generated modules
  • Controlled behavior for import following

mypy supports per-module sections in config files (the config docs cover per-module options).

Example pattern (conceptual)

  • Make app.* strict
  • Make tests.* permissive
  • Make legacy.* “don’t block the world”

Import Handling: The Boundary Trap

Important nuance: exclude doesn’t mean “won’t be analyzed if imported”

Even if you exclude files from being directly checked, mypy can still follow imports and analyze them unless you configure import following behavior. This trips people up when they try to “exclude legacy”, but new code imports it.

Senior takeaway:

  • “What gets analyzed” is determined by entry points + import following, not only by exclude globs.

If you need to stop mypy from diving into certain imports, per-module controls like import-following behavior are part of the strategy.

Plugins (When Type Info Is Not Representable as Plain Stubs)

If a library uses heavy runtime metaprogramming (classic examples: model/ORM frameworks, validation libraries), mypy can’t infer enough from annotations alone.

Plugins are mypy’s escape hatch:

  • They let libraries teach mypy additional semantics

Pydantic example (why plugins matter)

Pydantic provides a mypy plugin that improves correctness for model fields and constructor signatures, catching errors that mypy otherwise can’t reliably detect.

Senior takeaway:

  • If your codebase depends heavily on a framework that generates attributes/signatures at runtime, check whether a mypy plugin exists and whether it’s considered stable for your version.

Performance & Developer Experience

Incremental mode and caching

mypy writes caches and can reuse them across runs for speed. Additionally, dmypy (daemon mode) keeps state in memory to make repeated checks much faster for larger codebases.

The mypy docs describe daemon caching requirements: the daemon needs fine-grained dependency data, enabled via --cache-fine-grained.

Practical patterns:

  • CI: mypy --cache-fine-grained ... to generate the right cache artifacts for daemon usage
  • Local dev: dmypy when the repo is large and “typecheck on save” matters

Error Codes and Suppression Policy

mypy usage treats # type: ignore as technical debt.

Key practices:

  • Prefer targeted ignores with error codes (where supported in your config/version)
  • Use warn_unused_ignores = true so dead ignores don’t accumulate
  • Gate new code more strictly than old code (per-module config)

Rule of thumb:

  • If ignores trend upward, the checker is losing.

Common Senior Pitfalls

  • Turning on strict globally without per-module strategy → adoption stalls
  • Using ignore_missing_imports = true globally → hides real integration bugs
  • Excluding legacy modules but still importing them → mypy still analyzes via imports
  • Not using framework plugins where necessary → “mypy says it’s fine” becomes meaningless (e.g., model libs)
  • Start with a baseline that improves safety without requiring full coverage
  • Make your core packages strict first
  • Use per-module loosening for tests/legacy
  • Eliminate Any at boundaries (I/O, network, parsing) over time
  • Add plugins only where runtime metaprogramming would otherwise defeat analysis

One-Sentence Mental Model

mypy configuration is how you choose:

  • what is in scope,
  • how strict correctness must be,
  • and where you permit gradual-typing escape hatches, so that type checking stays enforceable instead of aspirational.