5 min read

Pre-commit in Python: Isolated Environments for Linters and Formatters

Jakub Kriz

Jakub Kriz

Software Engineer & Python Enthusiast

Pre-commit in Python: Isolated Environments for Linters and Formatters

Code formatters improve readability by enforcing consistent style, but unlike Go or Rust, Python lacks built-in, standardized formatting tools. Furthermore, it doesn’t have a compiler to catch obvious errors like unused variables or type mismatches. Instead, the ecosystem compensates with distinct tools: formatters (like Black) focus purely on code style; linters (like Flake8) enforce rules for logical correctness; and type checkers (like mypy) validate type annotations. Without automation, remembering to run each tool consistently becomes an error-prone chore.

Why Pre-Commit?

Despite its name, Pre-commit is more than just a Git hook manager—it acts as a package manager for formatting and linting tools, ensuring they run consistently across different environments. It unifies pre-commit hooks into a single configuration file, abstracting away installations and environment setups. Each tool runs in isolated environments (like ephemeral virtualenvs), ensuring clean and reproducible executions. The config also centralizes file exclusion rules, replacing tool-specific settings scattered across pyproject.toml, .flake8, and more. Although configuring Pre-commit requires some upfront effort, only one team member needs to set it up. Once done, the entire team benefits from a streamlined workflow for enforcing code quality.

Quick Start

  1. Install pre-commit through pip:
pip install pre-commit
  1. Create a .pre-commit-config.yaml file in the root of your repository:
---
default_language_version:
    python: python3.12
repos:
    -   repo: https://github.com/psf/black
        rev: 25.1.0  # Pin versions for stability
        hooks:
        -   id: black

    -   repo: https://github.com/pre-commit/mirrors-mypy
        rev: v1.13.0
        hooks:
        -   id: mypy
            exclude: ^tests/
            additional_dependencies:
                - pydantic
                - types-requests
  1. Run pre-commit commands

[!WARNING] Pre-commit requires a Git repository to function. Running these commands in a directory without .git will fail.

Install Hooks

# Install basic pre-commit hook (runs on git commit)
pre-commit install

# Install pre-push hook (runs on git push)
pre-commit install --hook-type=pre-push

Run Checks

# Validate just staged changes (default)
pre-commit run
# Validate entire codebase (including uncommitted files)
pre-commit run --all-files

Update Hooks

# Update all hooks to latest compatible versions
pre-commit autoupdate
  1. Set up pre-commit CI/CD

Properly configured pre-push hooks should prevent errors, but enforcing checks via a GitHub Action adds an extra layer of protection in case a developer bypasses local hooks.

- name: Run pre-commit
  uses: pre-commit/action@v3.0.0
  with:
    extra_args: --all-files

Isolated environments

The additional_dependencies in the mypy pre-commit hook are needed because of pre-commit’s isolated execution environment. When mypy analyzes your code, it needs access to type information for all imported libraries. Some libraries like requests don’t ship with built-in type hints - these live in separate types-* packages (like types-requests) that mypy would normally prompt you to install via mypy --install-types during regular use.

But pre-commit runs hooks in temporary, clean environments that don’t inherit your project’s dependencies. Since you can’t interactively run mypy --install-types through pre-commit, you must explicitly declare these type-stub packages in additional_dependencies so they’re automatically installed in the hook’s environment.

Even libraries with built-in type hints, like Pydantic, must be listed in additional_dependencies because Pre-commit runs in an isolated environment separate from your project’s virtual environment.

This setup achieves two things:

  1. Isolation - No need to install mypy/type-stubs in your project’s virtualenv
  2. Reproducibility - Type-checking works the same everywhere, regardless of a contributor’s local setup

One minor caveat is when a library, such as Pydantic, is required both as a runtime dependency and for type checking. In such cases, you’ll need to list it in both your project’s requirements and in additional_dependencies.

Useful Hooks

  • pre-commit-hooks
    • Various small ‘official’ hooks
  • black
    • Widely used Python formatter designed for consistency and minimal configuration.
    • References Henry Ford’s quote: “Any color… as long as it’s black”
  • isort
    • Automatically sorts Python imports
  • flake8
    • Most widely used Python linter
  • ruff
    • Modern linter/formatter (written in Rust)
    • Can replace Black, isort, and Flake8 to some degree
  • mypy
    • Static type checker for Python
  • shellcheck
    • Linter for shell scripts (Bash/Sh)
  • yamllint
    • Linter for YAML files
  • actionlint
    • Linter for GitHub Actions workflows
  • pymarkdown
    • Markdown linter

Conclusion

Pre-commit is a tool that automates code quality checks by managing Git hooks and consolidating formatters, linters, and type checkers through a centralized configuration file and isolated environments. It ensures consistent, reproducible quality checks across local and CI/CD workflows, streamlining development and reducing manual overhead.