Pyright vs Mypy: Architecture, CI Workflows & Strictness Comparison
Choosing between Pyright type checking speed vs mypy requires understanding their underlying execution models, configuration paradigms, and CI/CD integration patterns. This guide provides a technical comparison focused on architectural divergence, strictness alignment, and actionable debugging workflows for production environments.
Teams evaluating Static Analysis Tools & CI Integration should prioritize execution speed, incremental analysis capabilities, and ecosystem compatibility when selecting a primary type checker. Key architectural differences dictate how each tool scales.
mypy relies on Python-native AST traversal, while pyright leverages a TypeScript-based language server. Strictness configuration mapping requires careful translation to avoid behavioral gaps. CI pipeline optimization demands parallel execution, incremental caching, and deterministic failure gating.
Architectural Execution Models & Performance Characteristics
mypy operates as a standalone Python script that parses source files into an abstract syntax tree. It performs semantic analysis sequentially by default, though --jobs N enables parallel processing. This can become a bottleneck in monorepos exceeding 500k lines without the dmypy daemon.
Memory consumption scales with the size of the type graph during full-repo scans. Developers often use --follow-imports=skip or targeted directory analysis to stay within CI runner limits.
Pyright runs on Node.js and implements the Language Server Protocol. It uses multi-threaded parsing and incremental file watching. This architecture drastically reduces cold-start times and enables background analysis during development.
For large-scale repositories, pyright’s parallel execution model typically outperforms mypy by 3–5x on cold runs. It requires a Node.js runtime in the CI environment. Install via pip install pyright which bundles Node.js automatically.
When optimizing feedback loops, consider pairing either checker with Ruff Linter Integration to offload syntax validation and import sorting. This hybrid approach isolates type inference from stylistic checks.
Strictness Configuration & Type Inference Parity
Achieving equivalent strictness across both tools requires explicit configuration mapping. mypy’s strict = true flag aggregates over a dozen boolean toggles. Pyright uses typeCheckingMode with granular report* overrides.
Direct translation without validation will produce divergent false positives, stemming from differing type narrowing algorithms and stub resolution orders.
# pyproject.toml (mypy)
[tool.mypy]
strict = true
warn_return_any = true
warn_unused_configs = true
// pyrightconfig.json (pyright)
{
"typeCheckingMode": "strict",
"reportUnnecessaryTypeIgnoreComment": true,
"reportMissingTypeStubs": true
}
This demonstrates how to align strictness baselines across both tools. Pyright’s granular reporting flags allow enabling only specific checks, while mypy’s boolean strict toggle is all-or-nothing.
Third-party stub resolution differs significantly. mypy prioritizes typeshed and MYPYPATH. Pyright defaults to typings/ directory and pythonPath resolution.
Behavioral divergence in implicit Any inference is common. Pyright treats untyped function parameters as Unknown by default. mypy defaults to Any unless disallow_any_unimported is set. This means pyright catches more issues by default in untyped code.
Unknown (a stricter concept than Any) by default in strict mode, catching more issues in untyped code than mypy does with its default Any inference. If you switch from mypy to pyright, expect a wave of new errors in code that was previously "clean" under mypy.
For baseline strictness tuning before cross-tool migration, reference Mypy Configuration & Strictness to establish a controlled rollout strategy.
CI/CD Integration Patterns & Workflow Automation
Embedding type checkers into modern CI pipelines requires deterministic gating and cache-aware execution. Running both tools in a matrix strategy prevents sequential bottlenecks. Incremental caching must account for Python version, dependency lockfiles, and checker binaries.
name: Type Check Matrix
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
strategy:
matrix:
checker: [mypy, pyright]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
- run: pip install ${{ matrix.checker }}
- run: ${{ matrix.checker }} src/
This shows a scalable CI pattern that runs both checkers in parallel. For mypy, append --cache-dir .mypy_cache and cache that directory to preserve incremental state. For pyright, --stats outputs memory usage and timing.
Handle flaky type errors by excluding dynamically generated modules via exclude directives. Establish baseline coverage thresholds before enabling strict mode enforcement.
Debugging Complex Inference Failures & Modern Syntax
Advanced Python features expose engine-specific inference gaps. Systematic debugging requires isolating the failing expression and inspecting the inferred type hierarchy.
Start by inserting reveal_type(variable) at the failure point. Both checkers will emit the resolved type to stderr.
from typing import TypeGuard
def is_valid_config(data: dict[str, object]) -> TypeGuard[dict[str, str]]:
return all(isinstance(v, str) for v in data.values())
def process(data: dict[str, object]) -> None:
if is_valid_config(data):
reveal_type(data) # Both mypy and pyright: dict[str, str]
If narrowing fails in a match block, ensure all cases explicitly return or raise. Pyright handles structural subtyping more aggressively. mypy requires explicit isinstance guards for TypeGuard validation.
Protocol and TypedDict compatibility discrepancies often stem from total=False handling. Pyright permits partial matches by default in some contexts. mypy enforces exact key presence.
Validate PEP 695 support by pinning checker versions. Require mypy >=1.12 (where the new syntax is enabled by default) and pyright >=1.1.320. Older versions reject or ignore type keyword syntax.
Common Implementation Pitfalls
- Assuming strict mode parity guarantees identical error reporting: mypy and pyright implement different type narrowing algorithms and stub resolution orders. Directly copying strict flags without validating against your codebase will produce divergent false positives and missed errors.
- Ignoring incremental cache invalidation in CI pipelines: Both tools cache analysis results, but cache keys must include Python version, dependency hashes, and checker version. Stale caches cause phantom passes or unexplained failures in PR checks.
- Overusing
# type: ignoreinstead of fixing underlying inference gaps: Suppressing errors masks architectural type leaks. Usereveal_type()to inspect inferred types and refactor function signatures or add explicitTypeAliasdeclarations before applying suppression.
Frequently Asked Questions
Can I run pyright and mypy simultaneously in the same CI pipeline? Yes, but run them in parallel matrix jobs to avoid compounding execution time. Use separate cache directories and ensure both check against the same dependency lockfile to maintain consistency.
Which tool handles PEP 695 type parameter syntax better?
Both support PEP 695, but pyright’s implementation aligns more closely with its TypeScript-based parser. mypy requires >=1.12 and an explicit --python-version 3.12 flag. Verify your Python version compatibility before enabling.
How do I migrate from mypy to pyright without breaking CI?
Start by running pyright in off or basic mode alongside mypy. Gradually enable standard and strict reporting categories, fixing divergences incrementally before switching the CI gate.
Does pyright replace the need for a separate linter? No. Pyright focuses on type inference and static analysis. Pair it with a dedicated linter like Ruff for style enforcement, import sorting, and fast syntax validation to maintain comprehensive code quality.