Optimizing mypy.ini for Large Codebases: Performance & Precision Tuning

TL;DR

Enable incremental = true and sqlite_cache = true in mypy.ini, scope strict checks to active modules via per-module overrides, and route untyped third-party imports through follow_imports = silent. For monorepos, use the dmypy daemon and pin cache directories to a persistent CI volume keyed on your lockfile hash.

Scaling static type checking across massive Python repositories requires precise configuration of Mypy Configuration & Strictness parameters. This guide details exact tuning strategies for incremental caching, per-module strictness scoping, and third-party import routing. You will eliminate CI bottlenecks while maintaining rigorous type safety within broader Static Analysis Tools & CI Integration workflows.

mypy incremental cache flow The module graph splits into unchanged modules read from the SQLite cache (fast path) and changed modules that are fully re-parsed and re-checked (slow path). Both paths merge into a final type-check result. Module graph all .py files Unchanged modules read from .mypy_cache Changed modules full re-parse + check Type-check result errors + warnings fast path ⚡ slow path (only what changed)
Incremental mode re-checks only changed modules; unchanged modules are served from the .mypy_cache SQLite store, cutting CI time dramatically on large codebases.

Incremental Caching & SQLite Backend Tuning

Enable persistent caching to bypass cold-start overhead. Set cache_dir to a volume that survives CI job restarts. For repositories exceeding 100k LOC, consider the SQLite backend to reduce filesystem inode pressure during concurrent worker execution.

[mypy]
python_version = 3.11
incremental = true
sqlite_cache = true
cache_dir = .mypy_cache
show_error_codes = true
warn_return_any = true
warn_unused_ignores = true
follow_imports = silent

This configuration enables persistent cache routing and strict error reporting. It silences untyped third-party traversal to prevent AST bloat. Note that sqlite_cache requires mypy >=0.900. Python 3.10+ is recommended for stable PEP 604 union syntax. Run mypy --cache-fine-grained to enable finer-grained invalidation that only rebuilds affected modules. Run mypy --cache-dir /dev/null (or --no-incremental) only during major interpreter upgrades to force a clean baseline.

Per-Module Strictness Overrides for Gradual Adoption

Apply granular strictness via per-module overrides instead of global flags. Define base [mypy] defaults first. Then scope exceptions to legacy paths. This prevents strictness leakage while isolating warn_return_any to active development paths.

[mypy-legacy.*]
disallow_untyped_defs = false
ignore_errors = true

[mypy-app.core.*]
disallow_untyped_defs = true
strict_equality = true

This pattern isolates strict checking to active modules while bypassing legacy code. It maintains global coverage metrics without degrading developer velocity. Consider this Python 3.10+ module that leverages the strict config:

# src/app/core/models.py
from typing import Protocol, TypeAlias

class DataProcessor(Protocol):
    def process(self, data: bytes) -> str: ...

HandlerType: TypeAlias = DataProcessor | None

def execute(handler: HandlerType) -> str:
    if handler is None:
        raise ValueError("Handler required")
    return handler.process(b"payload")  # mypy validates strict equality & return types

Pyright and Ruff handle strictness differently. Ruff uses pyproject.toml rule codes. Pyright relies on typeCheckingMode and exclude arrays. Stick to mypy’s INI overrides for precise path scoping. Version constraints matter: mypy >=1.4.0 optimizes protocol checking for this pattern.

Third-Party Import Routing & Stub Path Optimization

Route untyped dependencies through custom stub directories instead of ignoring imports globally. Configure mypy_path to point to internal type definitions. Use follow_imports = silent for known-untyped packages — this suppresses noise while still reading .pyi stubs.

follow_imports = skip vs silent Never use follow_imports = skip for third-party libraries — it silently drops all type information from those imports, causing false negatives. Use follow_imports = silent instead: it suppresses error output from untyped dependencies while still reading any available .pyi stubs.
[mypy]
mypy_path = ./typeshed_custom

[mypy-pandas.*]
ignore_missing_imports = true

[mypy-numpy.*]
ignore_missing_imports = true

This routes internal type definitions to a dedicated folder while explicitly ignoring specific heavy dependencies. It reduces memory footprint without breaking type propagation for first-party code. Avoid ignore_missing_imports = true at the root [mypy] level — it masks legitimate missing hints in first-party code.

CI Memory Constraints & Parallel Execution

Prevent OOM kills by limiting parallel workers and monitoring RSS. Set show_traceback = false in production CI runs to reduce memory overhead. Control concurrency via the --jobs flag.

# CI Pipeline Configuration
mypy --config-file mypy.ini --jobs 4 src/

Use --no-incremental strategically for nightly full scans. This catches cache drift without impacting daily PR checks. mypy’s worker model (--jobs) scales well on wide module graphs but provides less benefit on deep sequential chains. Monitor your runner’s RSS and adjust accordingly. Python 3.11+ improves the interpreter’s baseline memory usage.

Common Mistakes

  • Disabling incremental mode globally: Forces full AST rebuilds on every run, increasing CI times from seconds to minutes. It negates mypy’s primary scaling mechanism.
  • Using follow_imports=skip for untyped libraries: Prevents reading .pyi stubs entirely. This causes false negatives and breaks type propagation across module boundaries. Use follow_imports=silent instead.
  • Applying ignore_missing_imports = true globally: Silences legitimate missing type hints in first-party code. This masks critical integration errors and degrades coverage metrics. Apply it per-module via [[mypy-package.*]] sections.

FAQ

How do I prevent mypy cache corruption in shared CI environments? Isolate cache directories per branch or job ID using environment variables (MYPY_CACHE_DIR). Run mypy --cache-dir /tmp/fresh_cache when you suspect corruption rather than deleting the shared cache.

Should I use follow_imports=skip to speed up large monorepos? No. Use follow_imports=silent instead. It preserves type inference from .pyi stubs while suppressing error output from untyped dependencies. skip silently drops type information from those imports.

How can I enforce strict typing only on newly added files? Combine git diff --name-only with a pre-commit hook that passes a dynamic file list to mypy. Maintain per-module overrides in mypy.ini for legacy paths to avoid false positives.

What is the optimal cache directory location for Dockerized CI runners? Mount a persistent volume or use GitHub Actions cache keys to preserve .mypy_cache across workflow runs. Ensure the volume is writable by the CI user and keyed on the lockfile hash to avoid stale caches after dependency updates.

Back to Mypy Configuration & Strictness