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

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

These 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: false

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

Learn More

Last updated on