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.inisetup.cfgpyproject.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 = Truefor 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 disciplinedcheck_untyped_defs = truedisallow_incomplete_defs = true
# Keeps Optional honestno_implicit_optional = true
# Hygienewarn_unused_ignores = truewarn_redundant_casts = trueno_implicit_reexport = true
# Boundary controlignore_missing_imports = falseWhy 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:
dmypywhen 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 = trueso 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
strictglobally without per-module strategy → adoption stalls - Using
ignore_missing_imports = trueglobally → 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)
Recommended Adoption Strategy (Broad, Practical)
- 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
Anyat 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.