Skip to main content

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__.py required)
  • warn_return_any — Warn if a function returns Any (often indicates missing type info)
  • warn_unused_ignores — Warn about # type: ignore comments that don't suppress any errors
  • warn_redundant_casts — Warn about unnecessary cast() calls
  • follow_imports — How to handle imported modules: silent (don't check), normal (check if available), skip (skip entirely)
  • no_implicit_optional — Forbid None types without explicit Optional
  • cache_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:

AspectmypyPyright
StrictnessModerate; gradual typing-friendlyVery strict; finds more edge cases
PerformanceSlower (especially first run)Much faster; good for large codebases
PEP complianceFocuses on PEP 484, 586, 589Strict PEP adherence
IDE integrationVia plugins; slowerExcellent; built for VS Code
Type stubsUses typeshedUses 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-errors for generated code or legacy modules
  • For monorepos, share a global mypy.ini and 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