Automating Pre-Commit Type Validation for Python Projects
Pin your type checker version in .pre-commit-config.yaml via additional_dependencies, set pass_filenames: false for mypy and pyright, and use a files: regex to scope hooks to your source directory. Mirror the same config in CI by running pre-commit run --all-files with a cached .mypy_cache keyed on your lockfile hash.
Shift type validation to the commit stage to enforce consistency without blocking developer velocity. This guide provides exact hook syntax, environment isolation strategies, and CI parity configurations to automate type checking with minimal overhead. Isolate checker dependencies and apply incremental strictness to maintain velocity while guaranteeing type safety.
Hook Configuration & Environment Isolation
Pre-commit creates isolated virtual environments for each registered hook. Relying on the project’s global environment causes version drift and ModuleNotFoundError during execution. Explicitly pin type checker versions and third-party stubs using additional_dependencies. This ensures deterministic analysis regardless of local site-packages state.
For comprehensive type graph resolution, set pass_filenames: false. When pass_filenames is true, checkers only analyze staged files — this breaks cross-module inference and generates false negatives. Align your local hook architecture with broader Static Analysis Tools & CI Integration pipelines to prevent environment fragmentation.
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.17.0
hooks:
- id: mypy
name: mypy (strict)
args: [--strict, --ignore-missing-imports, --show-error-codes]
additional_dependencies: [types-requests, types-PyYAML]
files: ^src/.*\.py$
pass_filenames: false
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.403
hooks:
- id: pyright
additional_dependencies: [pyright==1.1.403]
files: ^src/.*\.py$
pass_filenames: false
Validate the configuration against modern Python 3.10+ syntax. Type checkers require explicit stubs for newer typing constructs.
# src/models.py
from typing import TypeAlias, Self
from dataclasses import dataclass
Payload: TypeAlias = dict[str, float]
@dataclass(slots=True)
class Transformer:
def apply(self, data: Payload) -> Self:
return self
Incremental Strictness & Legacy Code Migration
Enforcing strict typing across legacy codebases causes immediate workflow friction. Use targeted flags and directory-scoped overrides to validate new modules while gracefully ignoring technical debt. mypy and pyright handle legacy paths differently: mypy relies on [[tool.mypy.overrides]], while pyright uses exclude arrays and typeCheckingMode.
Restrict hook execution to active development directories using the files: regex. Combine this with per-directory configuration to maintain strict standards for new features. Note that Ruff handles linting and formatting, not deep type inference. Keep Ruff in a separate hook stage to avoid conflating style violations with type errors.
[tool.mypy]
python_version = "3.11"
strict = true
[[tool.mypy.overrides]]
module = "legacy_module.*"
ignore_errors = true
ignore_missing_imports = true
[tool.pyright]
typeCheckingMode = "strict"
include = ["src"]
exclude = ["src/legacy_module"]
Parallel Execution & Cache Optimization for Large Repos
Unoptimized type checking can stall commits for minutes. Leverage native caching and AST reuse to reduce execution time. Configure mypy to persist .mypy_cache outside the Git tree to prevent cache invalidation on branch switches.
Exclude heavy directories like venv, .tox, and generated protobuf files via the exclude: regex. Pyright’s --stats flag helps identify bottlenecks during initial tuning.
- name: Cache mypy
uses: actions/cache@v4
with:
path: .mypy_cache
key: ${{ runner.os }}-mypy-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-mypy-
- name: Run pre-commit
run: pre-commit run --all-files --show-diff-on-failure
CI Parity & Exit Code Standardization
Local and CI environments must produce identical validation results. Standardize PYTHONPATH and MYPYPATH across runners to prevent false positives caused by missing stubs. Map non-zero exit codes directly to PR status checks using continue-on-error: false.
PYTHONPATH or MYPYPATH between developer machines and CI runners cause mypy to resolve stubs differently, producing false positives in one environment and false negatives in the other. Set both variables explicitly in your CI job environment and document them for local setup.
Execute pre-commit run --all-files --show-diff-on-failure in CI to surface exact failing lines. This configuration aligns with standardized Pre-commit Hooks Setup templates and ensures consistent gating across matrix runners.
Common Pitfalls & Fixes
- Missing
additional_dependencies: Pre-commit isolates environments. Omitting explicit pins causesModuleNotFoundErrorfor third-party stubs. Always declaretypes-*packages. - Incorrect
pass_filenamessetting: Full-repo graph analysis requirespass_filenames: falsefor type checkers. Otherwise, cross-module imports fail during partial commits. Linters like Ruff can safely usepass_filenames: true. - Global Python pollution: Local
pip installpackages leak into hook execution paths when usinglanguage: system. Rely onadditional_dependenciesor ensure your system Python is clean when usinglanguage: system.
FAQ
How do I prevent pre-commit from re-checking unchanged files?
Pre-commit caches hook results by file hash. Keep additional_dependencies and args static. Avoid --all-files locally to leverage incremental caching.
Why does mypy report “Cannot find implementation” in pre-commit but not locally?
Local runs inherit your project’s site-packages. Pre-commit uses an isolated venv. Add missing stubs to additional_dependencies or configure MYPYPATH explicitly.
Can I enforce type validation only on modified lines?
Static checkers require full-file AST parsing for accurate inference. Use files: regex to scope hooks to directories. Line-level validation is unsupported by both tools.
How to handle typing_extensions version drift?
Pin typing_extensions in additional_dependencies alongside your type checker. Mismatched versions trigger AttributeError on newer syntax like TypeAlias or ParamSpec.