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 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.15.12
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-formatThe ruff-check 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: v6.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 ty
ty does not have an official pre-commit hook repository yet (tracking issue). Use a local hook that runs ty through uvx:
- repo: local
hooks:
- id: ty
name: ty
entry: uvx ty check
language: system
types: [python]
pass_filenames: falseSetting pass_filenames: false lets ty discover files itself, which respects any exclude patterns in your pyproject.toml ty configuration.
Type checking with mypy
To run mypy as a pre-commit hook:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.0.0
hooks:
- id: mypy
additional_dependencies: []Note
mypy 2.0 changed several defaults that may surface new errors in code that passed under 1.x. See the mypy reference page for details. mypy 2.0 also requires Python 3.10 or newer as both a runtime and a target.
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: v6.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.15.12
hooks:
- id: ruff-check
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-check --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. AI coding agents reach for --no-verify more readily than humans do; see how to stop AI agents from bypassing pre-commit hooks for the defensive setup.
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@v7
- 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