Pyright vs Mypy: Optimizing Type Checking Speed for Large Python Codebases
This guide isolates the architectural drivers behind type-checking latency. It provides exact configuration steps to minimize overhead in production pipelines. While broader architectural differences are covered in our Pyright vs Mypy Comparison, this page focuses exclusively on throughput optimization. You will learn cache management and CI integration patterns for sub-second feedback loops.
Pyright leverages incremental AST parsing and background indexing. This enables near-instantaneous developer feedback. Mypy’s dmypy daemon eliminates repeated module loading overhead. It requires explicit cache directory management. CI pipeline timeouts typically stem from unoptimized exclude patterns. Proper heap allocation and parallel execution reduce wall-clock time by 40-70%.
Baseline Benchmarking & Profiling Setup
Establish reproducible performance metrics before applying optimizations. Use standard time and py-spy to capture real versus CPU time during full analysis runs. Isolate third-party stub generation from core type resolution. Configure consistent PYTHONPATH and MYPYPATH to prevent cache invalidation loops.
Create a minimal Python 3.10+ target to validate baseline speeds. Use modern syntax like TypeAlias and match statements to stress-test parser performance.
# src/sample_module.py
from __future__ import annotations
from typing import TypeAlias, Protocol
DataPayload: TypeAlias = dict[str, int | float]
class Validator(Protocol):
def validate(self, payload: DataPayload) -> bool: ...
def process_data(data: DataPayload, validator: Validator) -> None:
if validator.validate(data):
print("Validated successfully.")
Run baseline checks with explicit version constraints. Pyright requires >=1.1.330 for stable incremental parsing. Mypy requires >=1.6.0 for reliable daemon caching.
# Baseline timing
time pyright src/
time mypy src/
Pyright Incremental Analysis & Watch Mode Tuning
Configure Pyright to skip unchanged modules. Optimize memory allocation for faster CI execution. Enable typeCheckingMode: "strict" with useLibraryCodeForTypes: false to bypass external parsing. Set watchForSourceChanges: true and watchForLibraryChanges: false in CI to prevent redundant scans. Leverage --outputjson for structured CI parsing without regex overhead.
# pyproject.toml
[tool.pyright]
typeCheckingMode = "strict"
useLibraryCodeForTypes = false
watchForSourceChanges = true
watchForLibraryChanges = false
exclude = ["**/tests", "**/migrations", "**/node_modules"]
This configuration disables expensive library parsing. It restricts scanning to source directories. Initial load time drops by approximately 60%.
Mypy Daemon (dmypy) & Cache Invalidation Strategies
Replace standard CLI invocations with persistent daemon processes. This enables rapid re-checks across CI steps. Initialize with dmypy start -- --cache-dir .mypy_cache --follow-imports skip only on the first run. Use dmypy run -- --follow-imports silent to skip dependency traversal. Implement explicit cache pruning to prevent stale metadata.
# Initialize daemon (first run only)
dmypy start -- --cache-dir .mypy_cache --follow-imports skip
# Execute incremental check
dmypy run -- --config-file pyproject.toml
# Teardown to free memory
dmypy stop
# Prune stale cache metadata (run weekly or post-upgrade)
find .mypy_cache -name '*.meta.json' -mtime +7 -delete
Maintaining a persistent process eliminates Python interpreter startup overhead. It removes repeated module import costs. Cache pruning prevents exponential growth in monorepos.
CI Pipeline Integration for Sub-Second Feedback
Embed optimized type checking into Static Analysis Tools & CI Integration workflows without blocking PR merges. Run type checks in parallel with linting and unit tests using matrix strategies. Implement --exit-zero for baseline generation. Enforce strict thresholds on deltas only. Use pre-commit pass_filenames: true to restrict checks to staged files.
Note the toolchain divergence: Ruff handles linting and formatting at Rust-native speeds. It does not perform type resolution. Run Ruff first to catch syntax errors. Execute Pyright or Mypy in parallel for semantic validation.
# .pre-commit-config.yaml
- repo: local
hooks:
- id: pyright-fast
name: Pyright (Incremental)
entry: bash -c 'pyright --outputjson --watch --watchForLibraryChanges false'
language: system
types: [python]
pass_filenames: true
require_serial: false
This hook restricts execution to staged files. It disables library watching. Parallel pre-commit execution remains stable.
Memory Footprint & Garbage Collection Optimization
Prevent OOM kills and swap thrashing in monorepo environments. Limit Pyright heap via NODE_OPTIONS="--max-old-space-size=4096". Disable dmypy auto-restart on memory leaks by setting --timeout 300. Strip type stubs from site-packages during Docker image builds. This reduces index size and prevents unnecessary traversal.
# Dockerfile snippet for lean CI runners
RUN pip install --no-cache-dir mypy pyright
RUN find /usr/local/lib/python3.10/site-packages -name "*.pyi" -delete
ENV NODE_OPTIONS="--max-old-space-size=4096"
Exceeding 8GB typically yields diminishing returns. Garbage collection overhead dominates beyond that threshold.
Common Mistakes
Running standard mypy instead of dmypy in CI pipelines
The standard CLI reloads the entire AST and type environment on every invocation. This causes linear time increases with codebase size.
Enabling useLibraryCodeForTypes: true in Pyright for large projects
This forces Pyright to parse and index all third-party .py files. It bypasses pre-compiled .pyi stubs. Memory usage increases by 3-5x.
Omitting --follow-imports skip or silent in CI
This triggers recursive traversal of vendored dependencies. It causes exponential cache growth and frequent CI timeouts.
FAQ
Why does mypy cache become stale after dependency upgrades?
Mypy caches module hashes based on file content and PYTHONPATH. Upgrading dependencies changes underlying type stubs without invalidating the cache. Run dmypy stop && rm -rf .mypy_cache after pip install -U.
How do I prevent Pyright from blocking pre-commit hooks?
Use pass_filenames: true in your .pre-commit-config.yaml and set watchForSourceChanges: true. This restricts Pyright to analyze only staged files rather than the entire workspace.
What is the optimal memory allocation for type checking in CI runners?
Set NODE_OPTIONS="--max-old-space-size=4096" for Pyright. Limit dmypy workers to 2-4 via --jobs 4. Exceeding 8GB typically yields diminishing returns due to GC overhead.