pre-commit Hooks
pre-commit runs linting, formatting, and type checks automatically before each git commit. Problems get caught locally in seconds instead of in CI minutes later.
Recipe
uv add --group dev pre-commit
uv run pre-commit install# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatuv run pre-commit run --all-filesWhen to reach for this:
- Developers forget to run lint before pushing
- CI fails on formatting nits that slow reviews
- New team members need guardrails from day one
Working Example
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.0
hooks:
- id: mypy
additional_dependencies: [pydantic>=2]
args: [src/]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: [--maxkb=500]uv run pre-commit install
git commit -m "feat: add billing module"
# ruff + mypy run automatically; commit blocked on failureWhat this demonstrates:
- Hooks run on staged files only (fast)
--fixauto-corrects before blocking- mypy hook type-checks with project dependencies
- Generic hooks catch whitespace and large file accidents
Deep Dive
How It Works
pre-commit installwrites a.git/hooks/pre-commitscript- On
git commit, each hook runs in an isolated environment - Failed hooks block the commit; fixed files may need re-staging
pre-commit run --all-filessimulates a full run (useful in CI)
Hook Performance
| Hook | Typical time | Scope |
|---|---|---|
| ruff | <1s | Staged files |
| ruff-format | <1s | Staged files |
| mypy | 5-30s | Configured paths |
| trailing-whitespace | <1s | Staged files |
Gotchas
- Slow mypy on every commit - frustrating for devs. Fix: run mypy in CI only; keep ruff in pre-commit.
- Not updating hook
rev- stale rule sets. Fix:pre-commit autoupdatequarterly. - Hooks not installed after clone - commits skip checks. Fix: document
pre-commit installin README; add CIpre-commit run --all-files. - Fix hooks change files but commit proceeds -
--exit-non-zero-on-fixre-stages issue. Fix: use the arg; re-add fixed files. - Different hook versions locally vs CI - inconsistent results. Fix: pin
revin config; CI runs same config.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| CI only | Very small team | Developers push broken code often |
| lefthook | Need faster parallel hooks | Ecosystem is smaller |
| husky | Monorepo with JS + Python | Python-only repo |
FAQs
How do I skip hooks once?
git commit --no-verify (use sparingly, never in team policy by default).
How do I run hooks manually?
pre-commit run --all-files or pre-commit run ruff --all-files.
Should mypy be in pre-commit?
For small projects yes. For large codebases, CI-only mypy is faster for dev flow.
How do I add a hook?
Add a repo entry to .pre-commit-config.yaml and run pre-commit install.
Do hooks work on Windows?
Yes. pre-commit supports Windows with Python installed.
How do I share config?
Commit .pre-commit-config.yaml. Every dev runs pre-commit install.
Can hooks run in CI?
Yes: pre-commit run --all-files as a CI step. Catches devs who skip install.
How do I exclude a file?
exclude: ^migrations/ in the hook config or types filter.
What if a hook needs project deps?
Use additional_dependencies in the hook config (as with mypy + pydantic).
How often to autoupdate?
Quarterly or when upgrading ruff/mypy versions in pyproject.toml.
Related
- Ruff Setup - ruff hook config
- mypy & pyright in CI - type checking
- Linting in CI/CD - CI pipeline
- Editor Integration - IDE feedback loop
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.