Advanced mypy Configuration: Pyright Integration
As your Python codebase grows, a single mypy.ini may no longer suffice. Real-world projects need fine-grained per-module configuration, integration with CI/CD pipelines, type coverage tracking, and coordination between mypy and Pyright. This article teaches you to configure mypy and Pyright for production-grade type checking across complex codebases.
Understanding mypy Configuration Options in Depth
Beyond basic settings, mypy offers powerful per-module and global controls. Here's an advanced mypy.ini for a complex project:
[mypy]
python_version = 3.10
namespace_packages = True
warn_return_any = True
warn_unused_configs = True
warn_redundant_casts = True
warn_unused_ignores = True
show_error_codes = True
show_error_context = True
pretty = True
# Strict mode
strict = True
# Import handling
ignore_missing_imports = False
follow_imports = silent
follow_imports_for_stubs = True
no_implicit_optional = True
# Mypy daemon for faster incremental checks
cache_dir = .mypy_cache
# Per-module overrides
[mypy-tests.*]
ignore_errors = True
disallow_untyped_defs = False
[mypy-third_party_lib]
ignore_missing_imports = True
[mypy-generated_code]
allow_any_expr = True
allow_any_unimported_types = True
Explanation of key options:
namespace_packages— Support PEP 420 namespace packages (no__init__.pyrequired)warn_return_any— Warn if a function returnsAny(often indicates missing type info)warn_unused_ignores— Warn about# type: ignorecomments that don't suppress any errorswarn_redundant_casts— Warn about unnecessarycast()callsfollow_imports— How to handle imported modules:silent(don't check),normal(check if available),skip(skip entirely)no_implicit_optional— ForbidNonetypes without explicitOptionalcache_dir— Where mypy stores incremental check cache for faster re-runs
Per-Module Configuration
Large projects need different strictness levels per module. Use [mypy-<pattern>] sections:
[mypy]
# Global settings: moderately strict
python_version = 3.10
strict = False
check_untyped_defs = True
# Core modules: maximum strictness
[mypy-myapp.core.*]
strict = True
# Tests: relaxed (don't require all annotations)
[mypy-tests.*]
ignore_errors = True
# Utilities: strict but allow some flexibility
[mypy-myapp.utils.*]
disallow_untyped_defs = True
disallow_incomplete_defs = False
# Legacy code: minimal checking
[mypy-myapp.legacy.*]
ignore_errors = True
# External libraries without stubs
[mypy-numpy.*]
ignore_missing_imports = True
[mypy-scipy.*]
ignore_missing_imports = True
This allows strict checking on critical paths (core business logic) while being lenient on tests and legacy code.
Type Coverage and Reporting
Track type coverage across your codebase—the percentage of code that has type hints. mypy can report this:
mypy --html coverage src/
This generates HTML reports in the coverage/ directory showing which files and functions lack annotations. Use this to prioritize typing efforts.
For metrics in CI/CD, generate JSON output:
mypy --json-report coverage src/
Then parse the JSON to track coverage trends over time.
Integrating Pyright as a Secondary Type Checker
Pyright is Microsoft's static type checker for Python, often stricter than mypy. Use both in CI/CD for complementary checks:
Install Pyright (requires Node.js):
npm install -g pyright
Or via Python:
pip install pyright
Create a pyrightconfig.json alongside your mypy.ini:
{
"include": ["src"],
"exclude": ["tests", "build", "dist"],
"pythonVersion": "3.10",
"pythonPlatform": "Linux",
"typeCheckingMode": "strict",
"venvPath": ".",
"venv": "venv",
"reportMissingImports": true,
"reportPrivateUsage": "warning",
"reportUnusedImports": "error",
"reportUnusedVariables": "warning",
"reportUnusedFunction": "warning",
"reportConstantRedefinition": "error"
}
Key differences between mypy and Pyright:
| Aspect | mypy | Pyright |
|---|---|---|
| Strictness | Moderate; gradual typing-friendly | Very strict; finds more edge cases |
| Performance | Slower (especially first run) | Much faster; good for large codebases |
| PEP compliance | Focuses on PEP 484, 586, 589 | Strict PEP adherence |
| IDE integration | Via plugins; slower | Excellent; built for VS Code |
| Type stubs | Uses typeshed | Uses typeshed; more complete |
Run both in CI/CD to catch issues each might miss:
mypy src/
pyright src/
CI/CD Integration: GitHub Actions Example
Here's a complete GitHub Actions workflow for type checking:
name: Type Check
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
mypy:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e . mypy types-requests
- run: mypy src/ --strict
- run: mypy --html coverage src/ || true # Generate coverage report (don't fail)
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: mypy-coverage-${{ matrix.python-version }}
path: coverage/
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- run: pip install pyright -e .
- run: pyright src/
type-coverage-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- run: pip install mypy -e .
- run: |
coverage=$(mypy --json-report /tmp/coverage src/ 2>/dev/null | python -c "import sys, json; data = json.load(sys.stdin); print(data['total_precision'])")
echo "Type coverage: $coverage%"
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "Type coverage below 80%"
exit 1
fi
This workflow:
- Runs mypy on multiple Python versions
- Runs Pyright for additional strictness
- Generates coverage reports (optional artifacts)
- Enforces a minimum type coverage threshold (80%)
Monorepo Configuration: Multiple Mypy Configs
For monorepos with multiple packages, use separate mypy.ini per package or a shared config:
monorepo/
├── mypy.ini (global config)
├── package_a/
│ ├── mypy.ini (package-specific, if needed)
│ └── src/
├── package_b/
│ └── src/
└── tests/
Global mypy.ini at the root:
[mypy]
python_version = 3.10
strict = True
namespace_packages = True
# Per-package overrides
[mypy-package_a.*]
# Could have different strictness if needed
[mypy-package_b.*]
# Different settings if needed
# Tests across all packages
[mypy-tests.*]
ignore_errors = True
Run mypy once at the root:
cd monorepo/
mypy # Checks both package_a and package_b per the config
Handling Common Real-World Scenarios
Scenario 1: Working with untyped third-party libraries
[mypy-requests.*]
ignore_missing_imports = True
[mypy-pandas.*]
ignore_missing_imports = True
Or install type stubs if available:
pip install types-requests types-pyyaml
Check typeshed or search PyPI for types-* packages.
Scenario 2: Gradual typing in legacy code
Don't enable strict = True globally. Instead, progressively type modules:
[mypy]
strict = False
check_untyped_defs = True
# Gradually add strict modules as they're typed
[mypy-myapp.api.*]
strict = True
[mypy-myapp.db.*]
strict = True
# Leave legacy modules lenient
[mypy-myapp.legacy.*]
ignore_errors = True
Track progress: "X of Y modules are now fully typed."
Scenario 3: Type checking scripts and entry points
Scripts often have more complex logic. Use relaxed settings:
[mypy-scripts.*]
disallow_untyped_defs = False
disallow_incomplete_defs = False
Scenario 4: Handling generated code
Generated code (e.g., from protobuf, OpenAPI) usually has no useful types:
[mypy-myapp.generated.*]
ignore_errors = True
Or add # mypy: ignore-errors at the top of generated files.
Practical Example: Complete Production Setup
Here's a complete, production-ready setup for a medium-sized project:
mypy.ini:
[mypy]
python_version = 3.10
plugins = mypy_django_plugin.main
django_settings_module = config.settings
namespace_packages = True
warn_return_any = True
warn_unused_configs = True
warn_redundant_casts = True
warn_unused_ignores = True
show_error_codes = True
strict = True
cache_dir = .mypy_cache
[mypy-tests.*]
ignore_errors = True
[mypy-migrations.*]
ignore_errors = True
[mypy-*.migrations.*]
ignore_errors = True
[mypy-django.*]
ignore_missing_imports = True
[mypy-rest_framework.*]
ignore_missing_imports = True
pyrightconfig.json:
{
"include": ["src"],
"exclude": ["tests", "migrations", "build"],
"pythonVersion": "3.10",
"typeCheckingMode": "strict",
"reportMissingImports": false,
"reportUnusedImports": "error",
"reportUnusedVariables": "warning"
}
Pre-commit hook (.pre-commit-config.yaml):
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args: [--strict]
additional_dependencies: [types-all]
- repo: https://github.com/RojerGS/pre-commit-pyright
rev: 1.1.289
hooks:
- id: pyright
Key Takeaways
- Use per-module configuration to enforce varying strictness across your codebase
- Run both mypy and Pyright in CI/CD for complementary checks
- Track type coverage percentage and set minimum thresholds (e.g., 80%)
- Integrate type checking into pre-commit hooks for instant feedback
- Use
# mypy: ignore-errorsfor generated code or legacy modules - For monorepos, share a global
mypy.iniand override per package if needed - Adopt gradual typing: start relaxed and progressively enable strict mode per module
Frequently Asked Questions
What's the difference between mypy cache modes?
mypy has two cache modes: normal (the default) increments the cache and daemon runs mypy as a background service for even faster checks. For CI, normal is fine; for local development, daemon mode is faster.
Should I commit the .mypy_cache directory?
No. Add it to .gitignore. The cache is machine and environment-specific. Regenerate it on CI.
Can I use mypy with async code?
Yes. mypy understands async/await and type-checks async functions. Type async functions like regular functions: async def func(x: int) -> str:.
How do I type check just changed files in a large codebase?
Use mypy --incremental (default) with the cache. Or run mypy on specific files: mypy src/modified_module.py.
What if Pyright and mypy disagree on a type error?
Both are correct per the spec; they just interpret edge cases differently. Choose one as your source of truth (usually mypy for gradual projects, Pyright for strict) and suppress the other's error if truly incompatible.
Further Reading
- mypy Advanced Configuration — Complete reference for all mypy.ini options
- Pyright Configuration — Full Pyright settings reference
- mypy Daemon for Faster Checking — How to use mypy daemon for CI/CD
- Type Coverage and Monitoring — Tools for tracking and improving type coverage