Skip to main content

Overview

Pre-commit hooks run automated checks on your code before it’s committed to version control. This ensures code quality and consistency across the team by catching issues early in the development process.
Pre-commit hooks run locally on your machine, providing immediate feedback before code even reaches CI/CD pipelines.

What’s Included

This project includes pre-commit hooks for Python code quality:
  • Ruff: Fast Python linter and code checker
  • Black: Uncompromising Python code formatter
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.6
    hooks: [{ id: ruff, args: [--fix] }]
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks: [{ id: black }]

Installation

Follow these steps to set up pre-commit hooks in your development environment:
1

Install pre-commit

Install the pre-commit package using pip:
pip install pre-commit
Or add it to your pyproject.toml dev dependencies:
[project.optional-dependencies]
dev = [
    "pre-commit>=3.5.0",
    # ... other dev dependencies
]
2

Install git hooks

Install the git hook scripts:
pre-commit install
This creates a .git/hooks/pre-commit script that runs automatically.
3

Verify installation

Test the hooks on all files:
pre-commit run --all-files
You should see output showing Ruff and Black running on your Python files.
Pre-commit hooks only work in git repositories. If you haven’t initialized git yet, run git init first.

Hook Details

Ruff

Ruff is an extremely fast Python linter written in Rust. It replaces multiple tools (Flake8, isort, pyupgrade, and more) with a single, blazingly fast linter. What it checks:
  • Code style violations (PEP 8)
  • Unused imports and variables
  • Common programming errors
  • Security issues
  • Import sorting
Configuration: The hook is configured with --fix to automatically fix issues where possible:
hooks: [{ id: ruff, args: [--fix] }]
Customize Ruff behavior in pyproject.toml:
[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "UP",  # pyupgrade
]
ignore = [
    "E501",  # line too long (handled by black)
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]  # Allow unused imports

Black

Black is “the uncompromising Python code formatter.” It reformats your code to ensure consistent style across your entire codebase. What it does:
  • Formats Python code to a consistent style
  • Handles line length (default 88 characters)
  • Manages whitespace and indentation
  • Formats strings, imports, and expressions
Configuration: Customize Black in pyproject.toml:
[tool.black]
line-length = 88
target-version = ['py312']
include = '\.pyi?$'
exclude = '''
/(
    \.git
  | \.venv
  | build
  | dist
)/
'''
Black is intentionally opinionated with minimal configuration options. This design choice eliminates debates about code style.

How Pre-commit Works

Normal Commit Flow

When you run git commit, pre-commit automatically:
1

Stage Detection

Identifies which files are staged for commit
2

Hook Execution

Runs each configured hook (Ruff, then Black) on staged files
3

Auto-fix

Applies automatic fixes where possible
4

Result

  • Pass: Commit proceeds normally
  • Fail: Commit is blocked, modified files are unstaged

When Hooks Fail

If pre-commit modifies files or finds unfixable issues:
$ git commit -m "Add new feature"
Ruff.....................................................................Failed
- hook id: ruff
- exit code: 1
- files were modified by this hook

Black....................................................................Passed
What to do:
  1. Review the changes made by the hooks:
    git diff
    
  2. Stage the fixed files:
    git add .
    
  3. Commit again:
    git commit -m "Add new feature"
    

Running Hooks Manually

Check All Files

Run hooks on all files in the repository:
pre-commit run --all-files
Useful when:
  • First setting up pre-commit
  • After changing hook configuration
  • Before making a pull request

Check Specific Files

Run hooks on specific files:
pre-commit run --files src/module.py tests/test_module.py

Run Individual Hooks

Run only one hook:
pre-commit run ruff --all-files
pre-commit run black --all-files

Skip Hooks

Temporarily skip hooks (use sparingly):
git commit -m "WIP: debugging" --no-verify
Skipping hooks with --no-verify bypasses quality checks. Use only when absolutely necessary, like committing broken code for debugging purposes.

Updating Hooks

Pre-commit hooks pin specific versions (e.g., rev: v0.5.6). Update them periodically:
pre-commit autoupdate
This updates .pre-commit-config.yaml with the latest versions:
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0  # Updated from v0.5.6
    hooks: [{ id: ruff, args: [--fix] }]
  - repo: https://github.com/psf/black
    rev: 24.8.0  # Updated from 24.4.2
    hooks: [{ id: black }]
After updating, test on all files:
pre-commit run --all-files

Adding More Hooks

Extend .pre-commit-config.yaml with additional hooks:

Type Checking with Mypy

- repo: https://github.com/pre-commit/mirrors-mypy
  rev: v1.11.0
  hooks:
    - id: mypy
      additional_dependencies: [types-requests]

Security Scanning with Bandit

- repo: https://github.com/PyCQA/bandit
  rev: 1.7.9
  hooks:
    - id: bandit
      args: ['-r', 'src/']

Markdown Linting

- repo: https://github.com/igorshubovych/markdownlint-cli
  rev: v0.41.0
  hooks:
    - id: markdownlint
      args: ['--fix']

General File Checks

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.6.0
  hooks:
    - id: trailing-whitespace
    - id: end-of-file-fixer
    - id: check-yaml
    - id: check-json
    - id: check-added-large-files
      args: ['--maxkb=1000']
    - id: check-merge-conflict

Team Setup

For New Team Members

Add setup instructions to your project README:
## Development Setup

1. Clone the repository
2. Install dependencies: `pip install -e ".[dev]"`
3. Install pre-commit hooks: `pre-commit install`
4. Verify setup: `pre-commit run --all-files`

Enforcing Pre-commit

While pre-commit hooks run locally, you can’t force team members to install them. Best practices:
1

Document Requirements

Add pre-commit setup to your contribution guidelines
2

CI Validation

Run the same checks in CI (already configured in workflows.yml)
3

PR Reviews

Check for formatting issues during code review
4

Automation

Consider using .github/workflows/pre-commit.yml to run hooks in CI:
name: Pre-commit
on: [pull_request]
jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - uses: pre-commit/[email protected]

Troubleshooting

If pre-commit doesn’t run on commit:
  1. Verify installation:
    pre-commit --version
    
  2. Check git hooks are installed:
    ls -la .git/hooks/pre-commit
    
  3. Reinstall if missing:
    pre-commit install
    
Ruff and Black can sometimes conflict on line length or formatting. To resolve:
  1. Ensure both use the same line length:
    [tool.ruff]
    line-length = 88
    
    [tool.black]
    line-length = 88
    
  2. Configure Ruff to ignore checks handled by Black:
    [tool.ruff.lint]
    ignore = ["E501"]  # Line length
    
If hooks take too long:
  1. Pre-commit only runs on changed files (not —all-files)
  2. Check for network issues (hooks download on first run)
  3. Consider running expensive checks (like mypy) only in CI
  4. Use pre-commit’s caching (enabled by default)
In emergencies, you can skip hooks:
git commit -m "Hotfix: critical bug" --no-verify
Then fix the code quality issues in a follow-up commit:
pre-commit run --all-files
git add .
git commit -m "Fix code quality issues"

Configuration Examples

Minimal Configuration

For a simple Python project:
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.6
    hooks:
      - id: ruff
        args: [--fix]
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black

Comprehensive Configuration

For production projects:
repos:
  # Python formatting
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.6
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
  
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black
  
  # Type checking
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.11.0
    hooks:
      - id: mypy
        additional_dependencies: [types-all]
  
  # Security
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.9
    hooks:
      - id: bandit
        args: ['-r', 'src/', '-ll']
  
  # General checks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-json
      - id: check-added-large-files
      - id: check-merge-conflict
      - id: detect-private-key

Best Practices

Install Immediately

Run pre-commit install as part of your initial project setup

Keep Updated

Run pre-commit autoupdate monthly to get latest improvements

Auto-fix When Possible

Use --fix args to reduce manual intervention

Mirror CI Checks

Run the same tools in CI and pre-commit for consistency

Build docs developers (and LLMs) love