Home Python Pre-commit Hooks for Python Projects: Automate Code Quality
Beginner 3 min · July 14, 2026

Pre-commit Hooks for Python Projects: Automate Code Quality

Learn how to use pre-commit hooks to automate linting, formatting, and testing in Python projects.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Python programming
  • Git installed and basic familiarity with Git commands
  • Python 3.6+ installed on your system
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Pre-commit hooks run checks before each commit to catch issues early.
  • They automate linting, formatting, and testing, ensuring code quality.
  • Setup is easy with a .pre-commit-config.yaml file.
  • Popular hooks include black, flake8, mypy, and isort.
  • They save time by preventing bad code from entering the repository.
✦ Definition~90s read
What is Pre-commit Hooks for Python Projects?

Pre-commit hooks are automated scripts that run quality checks on your code before each Git commit, helping you catch issues early and maintain a clean codebase.

Think of pre-commit hooks as a quality control inspector at a factory gate.
Plain-English First

Think of pre-commit hooks as a quality control inspector at a factory gate. Before a product (your code) leaves the factory (your local repo), the inspector runs a series of quick checks (linting, formatting, tests). If something is wrong, the product is sent back for fixes. This ensures only high-quality products reach the customers (your team or production).

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Imagine you're working on a Python project with a team. Everyone writes code in their own style, occasionally forgets to run tests, or leaves behind unused imports. The result? Pull requests filled with formatting inconsistencies, linting errors, and broken tests. Fixing these issues manually is tedious and error-prone. Enter pre-commit hooks: a powerful tool that automates code quality checks before every commit. Pre-commit hooks are scripts that run automatically when you run git commit. They can check for syntax errors, enforce coding standards, run tests, and even fix formatting. By catching issues early, they save time, reduce code review friction, and maintain a clean codebase. In this tutorial, you'll learn how to set up pre-commit hooks for Python projects, configure popular hooks like black, flake8, and mypy, and integrate them into your workflow. We'll also cover debugging common issues and best practices for production use. By the end, you'll have a robust pre-commit setup that ensures every commit meets your team's quality standards.

What Are Pre-commit Hooks?

Pre-commit hooks are scripts that run automatically before each commit in a Git repository. They are part of the Git hook system, which allows you to trigger actions at various points in the Git lifecycle. For Python projects, pre-commit hooks are commonly used to enforce code quality standards, run linters, format code, check for security issues, and run tests. The pre-commit framework (https://pre-commit.com) simplifies managing these hooks. Instead of writing shell scripts, you define hooks in a YAML configuration file, and the framework handles installation and execution. It supports hooks from multiple sources, including popular Python tools like black, flake8, isort, mypy, and many more. By integrating pre-commit hooks into your workflow, you ensure that every commit meets your project's standards, reducing the burden on code reviews and CI pipelines.

example.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
# Example of a simple pre-commit hook script (not using framework)
#!/usr/bin/env python
import sys
import subprocess

# Run flake8 on staged files
result = subprocess.run(['flake8', '--max-line-length=88'], capture_output=True, text=True)
if result.returncode != 0:
    print(result.stdout)
    sys.exit(1)
else:
    print("No linting errors found.")
    sys.exit(0)
🔥Why Use the Pre-commit Framework?
🎯 Key Takeaway
Pre-commit hooks automate quality checks before commits, and the pre-commit framework makes them easy to manage.

Setting Up Pre-commit Hooks

To get started, you need to install the pre-commit framework. It's a Python package, so you can install it via pip. It's recommended to install it in your virtual environment or globally. Once installed, you create a .pre-commit-config.yaml file in the root of your Git repository. This file defines the hooks you want to run. The basic structure includes a repos list, where each repo specifies a source (usually a Git repository) and hooks. For Python projects, common repos include https://github.com/psf/black for Black, https://github.com/PyCQA/flake8 for Flake8, and https://github.com/pre-commit/mirrors-mypy for Mypy. After creating the config, run pre-commit install to install the hook scripts into your .git/hooks/ directory. Now, every time you run git commit, the hooks will execute. You can also run all hooks on all files with pre-commit run --all-files.

.pre-commit-config.yamlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
repos:
  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        language_version: python3.11
  - repo: https://github.com/PyCQA/flake8
    rev: 6.1.0
    hooks:
      - id: flake8
        args: ['--max-line-length=88']
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.8.0
    hooks:
      - id: mypy
        additional_dependencies: ['types-requests']
  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ['--profile', 'black']
💡Pin Hook Versions
📊 Production Insight
In production, use a consistent Python version across all environments. Specify language_version in hooks to avoid mismatches.
🎯 Key Takeaway
Install pre-commit, create a .pre-commit-config.yaml with your desired hooks, and run pre-commit install to activate them.

Essential Python Hooks for Code Quality

Several hooks are indispensable for Python projects. Black is an opinionated code formatter that ensures consistent style. Flake8 checks for PEP 8 violations and common errors. isort sorts imports alphabetically and separates them into sections. Mypy performs static type checking, catching type errors before runtime. pylint is another linter that goes beyond PEP 8. For security, bandit finds common security issues. For testing, you can run pytest with a custom hook. Additionally, check-ast validates that files parse as valid Python. You can also include hooks for YAML, JSON, and other file types. The key is to choose hooks that match your project's needs and enforce them consistently.

.pre-commit-config.yamlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-ast
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace-fixer
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.6
    hooks:
      - id: bandit
        args: ['-s', 'B101']  # Skip assert statements
⚠ Avoid Overloading Hooks
📊 Production Insight
In large projects, consider using fail_fast: true to stop on the first hook failure, saving time.
🎯 Key Takeaway
Start with Black, Flake8, isort, and Mypy. Add security and testing hooks as your project grows.

Customizing Hook Behavior with Arguments

Many hooks accept arguments to customize their behavior. For example, Black supports --line-length to change the maximum line length. Flake8 can be configured with --max-line-length and --ignore to suppress specific errors. isort can use --profile black to align with Black's formatting. Mypy can be given --strict for stricter checks. You can also set environment variables or use configuration files like pyproject.toml or setup.cfg. The args key in the hook configuration allows you to pass command-line arguments. For complex configurations, it's better to use a dedicated config file and reference it.

.pre-commit-config.yamlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
repos:
  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        args: ['--line-length=100']
  - repo: https://github.com/PyCQA/flake8
    rev: 6.1.0
    hooks:
      - id: flake8
        args: ['--max-line-length=100', '--ignore=E203,W503']
  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ['--profile', 'black', '--line-length=100']
💡Use pyproject.toml for Shared Config
📊 Production Insight
When using pyproject.toml, ensure the pre-commit hook's version matches the tool version to avoid config incompatibilities.
🎯 Key Takeaway
Customize hooks with args or config files to match your project's coding standards.

Running Hooks on Specific Files or Stages

By default, pre-commit hooks run on all staged files. You can restrict hooks to specific file patterns using the files and exclude keys. For example, you might want to run a linter only on Python files or exclude generated files. You can also control when hooks run using the stages key. Stages include commit, push, merge-commit, etc. This is useful for running expensive hooks only during push, or running tests only on push. Additionally, you can use always_run: true to run a hook even if no files match. This is useful for hooks that check the entire repository, like a license check.

.pre-commit-config.yamlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-ast
      - id: check-yaml
        files: \.yaml$
  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        stages: [commit]
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.8.0
    hooks:
      - id: mypy
        stages: [push]
        exclude: ^tests/
🔥Stages for Different Workflows
📊 Production Insight
In CI, run pre-commit run --all-files --hook-stage push to simulate a push, catching issues that might be skipped during commit.
🎯 Key Takeaway
Use files, exclude, and stages to control when and on which files hooks run.

Debugging Pre-commit Hook Failures

Sometimes pre-commit hooks fail unexpectedly. Common issues include missing dependencies, incorrect configuration, or version mismatches. The first step is to run pre-commit run --all-files --verbose to see detailed output. If a hook fails due to a missing module, ensure the hook's additional_dependencies are specified. For example, mypy needs types-* packages for third-party libraries. If a hook modifies files (like Black or isort), you may need to stage the changes before committing. Use git diff to see what changed. If a hook is too slow, consider using fail_fast: true or moving it to a later stage. You can also skip hooks temporarily with git commit --no-verify, but use this sparingly.

debug_example.shPYTHON
1
2
3
4
5
6
7
8
9
# Run pre-commit on all files with verbose output
pre-commit run --all-files --verbose

# Check what files are staged
git diff --cached --name-only

# If a hook auto-fixes, stage the changes and recommit
git add -u
git commit -m "Apply pre-commit fixes"
⚠ Avoid --no-verify in Production
📊 Production Insight
In CI, set PRE_COMMIT_ALLOW_NO_CONFIG=1 to avoid failures if the config file is missing, but this is not recommended for production.
🎯 Key Takeaway
Debug failures with verbose output, check dependencies, and use git diff to review auto-fixes.

Integrating Pre-commit with CI/CD

Pre-commit hooks are not just for local development; they should also run in CI/CD pipelines to ensure consistency. Most CI systems can run pre-commit run --all-files as a step. This catches issues that might have been skipped locally (e.g., if a developer used --no-verify). You can also use the pre-commit.ci service, which automatically runs hooks on pull requests. To set it up, add a .pre-commit-config.yaml and enable the service. It will comment on PRs with hook failures. This provides a safety net and enforces standards across the team.

.github/workflows/pre-commit.ymlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
name: Pre-commit

on: [push, pull_request]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - uses: pre-commit/action@v3.0.0
💡Use pre-commit.ci for Free
📊 Production Insight
In CI, use pre-commit run --all-files --show-diff-on-failure to see the exact changes needed, making it easier for developers to fix issues.
🎯 Key Takeaway
Run pre-commit in CI to enforce standards on every push and pull request.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing Type Hints

Symptom
CI pipeline failed randomly with type errors, but local tests passed.
Assumption
Developers assumed the CI environment had different dependencies.
Root cause
Developers forgot to run mypy locally before pushing, and no pre-commit hook enforced it.
Fix
Added mypy as a pre-commit hook and configured it to run on all Python files.
Key lesson
  • Always run type checking before committing, not just in CI.
  • Use pre-commit hooks to enforce consistency across the team.
  • Include mypy in your pre-commit config to catch type errors early.
  • Document the pre-commit setup in your project's README.
Production debug guideSymptom to Action3 entries
Symptom · 01
Pre-commit hook fails with 'ModuleNotFoundError'
Fix
Ensure the hook's dependencies are installed in the environment where pre-commit runs. Use pre-commit install to set up the environment.
Symptom · 02
Hook passes locally but fails in CI
Fix
Check that the CI environment has the same pre-commit config and Python version. Use pre-commit run --all-files to replicate the CI run.
Symptom · 03
Hook takes too long to run
Fix
Optimize by using stages: [commit] to run only on commit, or use fail_fast: true to stop on first failure.
★ Quick Debug Cheat SheetCommon pre-commit issues and fixes
Hook not running
Immediate action
Run `pre-commit install` to install the git hook scripts.
Commands
pre-commit install
pre-commit run --all-files
Fix now
Reinstall hooks and run all files.
Hook fails with 'File not found'+
Immediate action
Check the hook's `files` or `exclude` patterns in config.
Commands
pre-commit run --verbose
pre-commit clean
Fix now
Adjust patterns or clean cache.
Hook modifies files unexpectedly+
Immediate action
Review the hook's behavior; some hooks auto-fix. Use `--no-verify` to bypass temporarily.
Commands
git diff
pre-commit run --all-files --verbose
Fix now
Commit the auto-fixed changes or adjust hook config.
ToolPurposeInstallationConfiguration
BlackCode formatterpip install blackpyproject.toml or CLI args
Flake8Linterpip install flake8.flake8 or setup.cfg
MypyStatic type checkerpip install mypymypy.ini or pyproject.toml
isortImport sorterpip install isort.isort.cfg or pyproject.toml
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
example.pyresult = subprocess.run(['flake8', '--max-line-length=88'], capture_output=True,...What Are Pre-commit Hooks?
.pre-commit-config.yamlrepos:Setting Up Pre-commit Hooks
debug_example.shpre-commit run --all-files --verboseDebugging Pre-commit Hook Failures
.githubworkflowspre-commit.ymlname: Pre-commitIntegrating Pre-commit with CI/CD

Key takeaways

1
Pre-commit hooks automate code quality checks, saving time and reducing errors.
2
Set up a .pre-commit-config.yaml with essential hooks like Black, Flake8, isort, and Mypy.
3
Customize hooks with arguments and stages to fit your workflow.
4
Integrate pre-commit into CI/CD to enforce standards across the team.
5
Debug failures with verbose output and ensure consistent environments.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What are pre-commit hooks and why are they useful?
Q02SENIOR
How do you configure a pre-commit hook to run only on Python files?
Q03SENIOR
Explain how to debug a pre-commit hook that fails in CI but passes local...
Q01 of 03JUNIOR

What are pre-commit hooks and why are they useful?

ANSWER
Pre-commit hooks are scripts that run automatically before each commit to catch issues early. They enforce code quality, consistency, and prevent bad code from entering the repository.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Do I need to install pre-commit globally or per project?
02
Can I use pre-commit hooks with other languages?
03
What if a hook modifies my files?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Advanced Python. Mark it forged?

3 min read · try the examples if you haven't

Previous
Ruff: Fast Python Linter and Formatter
25 / 35 · Advanced Python
Next
Python Logging: Production Patterns and Best Practices