Lint, Format, and Test Python on Every Commit with pre-commit and Ruff
Wire up pre-commit hooks so broken or unformatted Python is caught automatically before it ever reaches your branch.
What You'll Build
You'll configure pre-commit so that every git commit automatically runs Ruff (lint + format) and your pytest suite — blocking bad commits before they leave your machine.
Prerequisites
- Python 3.8+ — check with
python --version - Git with an initialized repo (
git init) - pip (bundled with Python 3.4+)
- A virtual environment activated (strongly recommended)
- Works on macOS, Linux, and Windows
1. Install the Tools
pip install pre-commit ruff pytest
Verify the installs:
pre-commit --version # e.g. pre-commit 3.7.0
ruff --version # e.g. ruff 0.4.4
2. Create the Hook Configuration
Create .pre-commit-config.yaml in your repo root:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff # linter; --fix applies safe auto-fixes
args: [--fix]
- id: ruff-format # formatter (replaces Black)
- repo: local
hooks:
- id: pytest
name: pytest
entry: pytest
language: system
pass_filenames: false
always_run: true
Tip: Replace
v0.4.4with the latest tag from github.com/astral-sh/ruff-pre-commit/releases, or runpre-commit autoupdateafter setup.
The local hook with language: system uses the Python that's currently active in your shell, so it picks up your project's installed dependencies automatically.
3. Install the Git Hooks
pre-commit install
Expected output:
pre-commit installed at .git/hooks/pre-commit
This writes a small script into .git/hooks/pre-commit that fires automatically on every git commit.
4. Add a Python File and a Test
Create calculator.py:
def add(a, b):
return a + b
Create test_calculator.py:
from calculator import add
def test_add():
assert add(2, 3) == 5
5. Stage and Commit
git add calculator.py test_calculator.py .pre-commit-config.yaml
git commit -m "Add calculator with pre-commit hooks"
The first run downloads the Ruff hook environment from GitHub (one-time, ~10 seconds). If all checks pass you'll see green Passed lines and the commit completes.
Verify It Works
Introduce a deliberate lint error — an unused import:
import os # unused
def add(a, b):
return a + b
Then try to commit:
git add calculator.py
git commit -m "test lint blocking"
Expected output (commit is blocked):
ruff.....................................................................Failed
- hook id: ruff
- exit code: 1
calculator.py:1:1: F401 `os` imported but unused
Because you passed --fix, Ruff removes safe issues automatically. Re-stage the corrected file and commit again to proceed.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
pre-commit: command not found |
pre-commit not on PATH | Activate your venv, or install globally with pipx install pre-commit |
ruff-format hook not recognized |
Pinned rev is too old (pre-v0.2.0) |
Run pre-commit autoupdate to bump revisions |
| pytest can't import your modules | venv not active when committing | Always commit from an active venv; language: system depends on it |
| Hooks run slowly on large repos | First run downloads + caches environments | Subsequent runs use the cache and are fast |
Next Steps
pre-commit autoupdate— bumps all hookrevpins to their latest releases in one command.- Ruff configuration — add a
[tool.ruff.lint]section inpyproject.tomlto enable extra rule sets (e.g.,select = ["E", "F", "I"]adds isort-style import sorting). - CI parity — run
pre-commit run --all-filesin GitHub Actions or your CI pipeline to catch issues on pull requests even if someone bypassed local hooks withgit commit --no-verify. - Type checking — once you're comfortable here, add a
mypyorpyrightlocal hook for static type analysis.
Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.
Discussion 0
No comments yet
Be the first to weigh in.