Pre-Commit Hooks¶
Run linters, formatters, and type checkers automatically on every git commit — and heavier checks, such as tests, on git push.
Install¶
uv add --dev pre-commit
uv run pre-commit install # activate hooks in repo
uv run pre-commit run --all-files # manual run on everything
.pre-commit-config.yaml¶
repos:
# --- General file hygiene ---
- 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-toml
- id: check-added-large-files
args: [--maxkb=500]
- id: detect-private-key
- id: check-merge-conflict
# --- Ruff (lint → format) ---
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.8
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
# --- mypy ---
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies:
- types-requests
- types-PyYAML
- pydantic
# --- wemake-python-styleguide ---
- repo: https://github.com/PyCQA/flake8
rev: "7.3.0"
hooks:
- id: flake8
args: [--select=WPS]
additional_dependencies:
- wemake-python-styleguide
Hook Ordering Rules¶
ruff-check --fixfirst — auto-fixes imports, unused vars, etc.ruff-formatsecond — reformats code after linter changes.mypythird — type-checks the clean code.flake8 --select=WPSlast — strictness layer.
Pre-Push Hooks¶
Git also runs a pre-push hook — right before git push sends commits to the remote.
The pre-commit framework manages it the same way: one config, different stages.
| Stage | Runs on | Put here | Time budget |
|---|---|---|---|
pre-commit |
git commit |
Formatting, linting, file hygiene — changed files only | Seconds |
pre-push |
git push |
Unit tests, full type check, secret scan, branch rules | Up to a minute |
Enable both stages¶
Tell pre-commit which hook types pre-commit install should set up, and keep existing hooks on the commit stage:
# .pre-commit-config.yaml (top level)
default_install_hook_types: [pre-commit, pre-push]
default_stages: [pre-commit] # hooks without `stages` run only on commit
uv run pre-commit install # installs both hooks from default_install_hook_types
uv run pre-commit install --hook-type pre-push # alternative: add only the pre-push hook
Without default_stages
Hooks that don't declare stages run at every installed stage. After installing pre-push,
Ruff, mypy and the rest would run a second time on every push.
Pre-push hooks¶
# --- Checks before push ---
- repo: local
hooks:
- id: pytest
name: pytest (fast unit tests)
entry: uv run pytest -q -m "not slow"
language: system
pass_filenames: false
stages: [pre-push]
- id: no-push-to-main
name: block direct push to main
entry: bash -c '[ "$(git rev-parse --abbrev-ref HEAD)" != "main" ] || { echo "Push to main is not allowed, open a PR"; exit 1; }'
language: system
pass_filenames: false
always_run: true
stages: [pre-push]
Existing hooks can move to push too — e.g. a slow full-project mypy run:
- id: mypy
stages: [pre-push]
Run and skip¶
uv run pre-commit run --hook-stage pre-push --all-files # run pre-push hooks manually
git push --no-verify # skip pre-push hooks once
Like pre-commit, the pre-push hook is client-side and can be skipped — CI remains the real gate.
Key Commands¶
uv run pre-commit run --all-files # check all files
uv run pre-commit autoupdate # update hook versions
uv run pre-commit run ruff-check # run single hook
SKIP=mypy git commit -m "wip" # skip specific hook once
CI Integration (GitHub Actions)¶
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: uv sync --locked
- run: uv run pre-commit run --all-files
Common Issues¶
| Problem | Fix |
|---|---|
| mypy can't find third-party types | Add stubs to additional_dependencies |
| Hook fails on generated files | Add exclude pattern to the hook |
| Slow mypy in pre-commit | Use pass_filenames: false + --incremental |
| Ruff version mismatch | Pin same version in .pre-commit-config.yaml and pyproject.toml |
Skip vs Commit Hooks in CI¶
| Strategy | Pre-commit | Pre-push | CI Pipeline |
|---|---|---|---|
| Purpose | Catch issues before commit (dev machine) | Catch failing tests before sharing (dev machine) | Gate before merge (server) |
| Speed | Must be fast (seconds) | Moderate (up to a minute) | Can be thorough (minutes) |
| Scope | Changed files only | Fast tests, full-project checks | Full repo, all tests |
| Can be skipped | Yes (--no-verify) |
Yes (--no-verify) |
No |
Run the same tools everywhere; pre-commit catches early, pre-push stops broken pushes, CI catches everything.