How to set up pre-commit hooks for a Python project
pre-commit is a framework that manages Git hooks. It runs tools like linters and formatters automatically before each commit, catching problems before they reach version control. This guide walks through installing pre-commit, configuring hooks for a Python project, and integrating it with Ruff and mypy.
Tip
prek is a faster, Rust-based drop-in replacement for pre-commit that reads the same configuration files. See the prek version of this guide for setup instructions.
Installing pre-commit
The easiest way to run pre-commit is through uvx, which runs it without adding it as a project dependency:
$ uvx pre-commit install
This installs a Git hook script in .git/hooks/pre-commit that runs automatically on git commit.
Creating the configuration file
pre-commit reads its configuration from a .pre-commit-config.yaml file in the project root. Here is a starting configuration that uses Ruff for linting and formatting:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatThe ruff hook runs the linter with auto-fix enabled. The ruff-format hook runs the formatter. They execute in order, so linting fixes are applied before formatting.
Tip
Run uvx pre-commit autoupdate periodically to update hook versions to the latest releases.
Adding more hooks
Trailing whitespace and file endings
The pre-commit-hooks repository provides several lightweight checks:
repos:
- 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-filesThese catch common issues: trailing whitespace, missing newlines at end of file, invalid YAML syntax, and accidentally committed large files.
Type checking with mypy
To run mypy as a pre-commit hook:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.16.0
hooks:
- id: mypy
additional_dependencies: []Add any type stub packages your project needs to additional_dependencies. For example, if your project uses requests:
additional_dependencies: [types-requests]A complete configuration
Combining the sections above into a single .pre-commit-config.yaml:
repos:
- 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
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatRunning hooks manually
To run all hooks against every file in the repository (not just staged files):
$ uvx pre-commit run --all-files
To run a specific hook:
$ uvx pre-commit run ruff --all-files
Skipping hooks temporarily
To bypass hooks for a single commit:
$ git commit --no-verify -m "WIP: work in progress"
Use this sparingly. Skipping hooks defeats the purpose of having them.
Using pre-commit in CI
pre-commit hooks run locally, but contributors might skip them. Running hooks in CI ensures every change is checked. Add this to a GitHub Actions workflow:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uvx pre-commit run --all-filesTroubleshooting
If hooks are not running on commit, verify the hook is installed:
$ uvx pre-commit install
If a hook fails because it cannot find dependencies, check that additional_dependencies is set correctly for hooks like mypy that need access to your project’s packages.
To clear the pre-commit cache (useful after updating Python or hook versions):
$ uvx pre-commit clean