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.
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of Python programming
- ✓Git installed and basic familiarity with Git commands
- ✓Python 3.6+ installed on your system
- 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.
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).
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
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.
language_version in hooks to avoid mismatches..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.
fail_fast: true to stop on the first hook failure, saving time.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.
pyproject.toml, ensure the pre-commit hook's version matches the tool version to avoid config incompatibilities.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 run --all-files --hook-stage push to simulate a push, catching issues that might be skipped during commit.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.
PRE_COMMIT_ALLOW_NO_CONFIG=1 to avoid failures if the config file is missing, but this is not recommended for production.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.
pre-commit run --all-files --show-diff-on-failure to see the exact changes needed, making it easier for developers to fix issues.The Case of the Missing Type Hints
- 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.
pre-commit install to set up the environment.pre-commit run --all-files to replicate the CI run.stages: [commit] to run only on commit, or use fail_fast: true to stop on first failure.pre-commit installpre-commit run --all-files| File | Command / Code | Purpose |
|---|---|---|
| example.py | result = subprocess.run(['flake8', '--max-line-length=88'], capture_output=True,... | What Are Pre-commit Hooks? |
| .pre-commit-config.yaml | repos: | Setting Up Pre-commit Hooks |
| debug_example.sh | pre-commit run --all-files --verbose | Debugging Pre-commit Hook Failures |
| .github | name: Pre-commit | Integrating Pre-commit with CI/CD |
Key takeaways
.pre-commit-config.yaml with essential hooks like Black, Flake8, isort, and Mypy.Interview Questions on This Topic
What are pre-commit hooks and why are they useful?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
That's Advanced Python. Mark it forged?
3 min read · try the examples if you haven't