Skip to content

How to set up pre-commit hooks for a Python project

This guide assumes you have a Python project managed with uv. If you haven’t created a project yet, see the project creation tutorial.

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-format

The 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-files

These 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-format

Running 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-files

Troubleshooting

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
Last updated on

Please submit corrections and feedback...