Pyright vs Mypy: Optimizing Type Checking Speed for Large Python Codebases

TL;DR

Pyright runs 3–5× faster than mypy on cold CI runs due to multi-threaded Node.js parsing. For mypy, use the dmypy daemon to eliminate repeated module loading between CI steps. Cache .mypy_cache and ~/.cache/pyright with lockfile-keyed keys, and set NODE_OPTIONS="--max-old-space-size=4096" for pyright on 8 GB runners.

This guide isolates the architectural drivers behind type-checking latency and 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: cache management and CI integration patterns for fast feedback loops.

Pyright leverages incremental AST parsing and background indexing, enabling near-instantaneous developer feedback. mypy’s dmypy daemon eliminates repeated module loading overhead but requires explicit cache directory management. CI pipeline timeouts typically stem from unoptimized exclude patterns or missing caches. Proper configuration reduces wall-clock time by 40-70% in most large codebases.

mypy vs pyright: cold run vs cached run On cold runs pyright is typically 3–5x faster than mypy. Both tools drop dramatically on warm cached runs. The dmypy daemon brings mypy warm-run times close to pyright. Time (relative) mypy cold dmypy pyright cold cached ~5x ~1x ~3x ~0.5x Cache key: pyproject.toml + requirements*.txt
Relative type-checking times: cold pyright beats cold mypy by 3–5×; the dmypy daemon closes the gap on warm runs.

Baseline Benchmarking & Profiling Setup

Establish reproducible performance metrics before applying optimizations. Use standard time 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.

# 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 >=1.1.330 provides stable incremental parsing. mypy >=1.6.0 provides reliable daemon caching.

# Baseline timing
time pyright src/
time mypy src/

Pyright Incremental Analysis Tuning

Configure pyright to skip unchanged modules and optimize memory allocation for CI execution. Set useLibraryCodeForTypes = false to bypass expensive third-party source parsing. Disable library watching in CI to prevent redundant scans. Use --outputjson for structured CI parsing.

# pyproject.toml
[tool.pyright]
typeCheckingMode = "strict"
useLibraryCodeForTypes = false
exclude = ["**/tests", "**/migrations", "**/node_modules"]

For local development, pyright’s VS Code extension handles file watching automatically. In CI, run pyright as a one-shot command — watch mode is not appropriate for batch CI jobs:

NODE_OPTIONS="--max-old-space-size=4096" pyright src/

This configuration disables expensive library source parsing and restricts scanning to source directories. Initial load time drops significantly for projects with large dependency trees.

Mypy Daemon (dmypy) & Cache Invalidation Strategies

Replace standard CLI invocations with persistent daemon processes to enable rapid re-checks across CI steps. Initialize on the first run and reuse across subsequent steps in the same job.

# Initialize daemon (first run only)
dmypy start -- --cache-dir .mypy_cache --config-file pyproject.toml

# Execute incremental check
dmypy run -- src/

# Teardown to free memory
dmypy stop

# Prune stale cache metadata (run when dependencies upgrade)
find .mypy_cache -name '*.meta.json' -mtime +7 -delete
Invalidate the cache after dependency upgrades mypy caches module hashes based on file content and PYTHONPATH. Upgrading dependencies changes the underlying type stubs without invalidating the stored hash. Run dmypy stop && rm -rf .mypy_cache after any pip install -U to force a clean rebuild and avoid phantom passes.

Maintaining a persistent process eliminates Python interpreter startup overhead and repeated module import costs. Cache pruning prevents exponential growth in monorepos after repeated dependency upgrades.

CI Pipeline Integration for Fast 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.

Note the toolchain division: Ruff handles linting and formatting at Rust-native speeds and does not perform type resolution. Run Ruff first to catch syntax errors, then execute pyright or mypy in parallel for semantic validation.

# .github/workflows/type-check.yml
name: Type Check
on: [push, pull_request]
jobs:
  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Cache mypy
        uses: actions/cache@v4
        with:
          path: .mypy_cache
          key: mypy-${{ hashFiles('pyproject.toml', 'requirements*.txt') }}
      - run: pip install mypy
      - run: mypy --config-file pyproject.toml src/

Memory Footprint & Resource Optimization

Prevent OOM kills and swap thrashing in monorepo environments. Limit pyright heap via NODE_OPTIONS="--max-old-space-size=4096". Limit dmypy concurrency via --jobs 4.

For Docker-based CI runners, install type stubs selectively rather than the full types-* collection:

# Dockerfile snippet for lean CI runners
RUN pip install --no-cache-dir mypy types-requests types-PyYAML pyright
ENV NODE_OPTIONS="--max-old-space-size=4096"

Do not delete .pyi stub files from site-packages — those stubs are exactly what mypy and pyright read for third-party type information. Removing them breaks type checking for all packages that ship stubs.

Common Mistakes

Running standard mypy instead of dmypy in multi-step CI jobs The standard CLI reloads the entire AST and type environment on every invocation. Use dmypy within a single job to persist state across steps. Between separate jobs, the daemon must be restarted — use the cache to speed up the cold start.

Enabling useLibraryCodeForTypes = true in pyright for large projects This forces pyright to parse and index all third-party .py source files rather than using pre-compiled .pyi stubs. Memory usage increases substantially.

Omitting --follow-imports silent in mypy CI This triggers recursive traversal of vendored dependencies. Use follow_imports = silent in mypy.ini to suppress error output from untyped third-party code while still reading their type stubs.

FAQ

Why does the 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 hash. Run dmypy stop && rm -rf .mypy_cache after pip install -U to force a clean rebuild.

How do I prevent pyright from blocking pre-commit hooks? Use pass_filenames: false with pyright scoped to your source directory. Pyright in pre-commit runs as a single batch check, not in watch mode.

What is the optimal memory allocation for type checking in CI runners? Set NODE_OPTIONS="--max-old-space-size=4096" for pyright on standard 8GB runners. For mypy, use --jobs 4 as a starting point and monitor RSS with time -v mypy ... (Linux) to adjust.

Back to Pyright vs Mypy Comparison