Skip to content

Pre-commit Hooks and Bootstrap Scripts

Pre-commit hooks and bootstrap scripts turn your setup and quality checks into repeatable commands instead of tribal knowledge. Together, they make it easy for every developer and every clone of the repository to use the same environment, tools, and automated checks with minimal effort.

Two Sides of the Same Coin

  • Bootstrap scripts answer "how do I set this project up?" with a single command
  • Pre-commit hooks answer "is this code ready to commit?" by running the same checks on every change before it reaches the remote repository

Why Use a scripts/ Folder?

A scripts/ (or tools/) directory in the project root gives you a dedicated home for small utilities and onboarding helpers such as bootstrap.sh and bootstrap.ps1.

Single Source of Truth

Instead of a long list of instructions in chat or documentation, you keep the authoritative setup steps in version-controlled scripts that can be evolved with the codebase.

What a Bootstrap Script Should Do

A typical bootstrap script for a Python project takes care of the repetitive tasks you want every developer to run:

  • Create or refresh the virtual environment if it does not exist yet
  • Install main and development dependencies in the right order
  • Install or update developer tooling, including pre-commit, test runners, and any project CLIs

This reduces onboarding to a single command:

./scripts/bootstrap.sh

One Command Setup

The script becomes the single source of truth for environment setup. Update it once when you change how dependencies or tools are installed, and every developer benefits immediately.


Example Bootstrap Scripts

Below are minimal examples you can adapt. They assume a .venv directory for the environment and requirements.txt plus requirements-dev.txt for dependencies.

scripts/bootstrap.sh
#!/usr/bin/env bash
set -euo pipefail

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE}")/.." && pwd)"
cd "$PROJECT_ROOT"

if [ ! -d ".venv" ]; then
python -m venv .venv
fi

# shellcheck disable=SC1091
source .venv/bin/activate

pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt

pre-commit install
scripts/bootstrap.ps1
$ErrorActionPreference = "Stop"

$ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $ProjectRoot\..

if (-not (Test-Path ".venv")) {
    python -m venv .venv
}

& .\.venv\Scripts\Activate.ps1

python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt

pre-commit install

This script creates the environment if needed, activates it, installs dependencies, and wires up pre-commit in one run.

Platform Consistency

PowerShell performs the same steps on Windows, giving every platform a one-command setup story. Include both scripts in your repo for maximum usability.


What Are Pre-commit Hooks?

Git hooks are scripts that run at specific points in the Git workflow, such as before a commit is created. The pre-commit hook runs just before Git finalizes the commit, allowing checks to block bad changes.

The pre-commit Framework

The pre-commit framework is a tool that manages these hooks for you: you declare a list of hooks in .pre-commit-config.yaml, and it installs and runs them automatically on the staged files whenever you commit.

Why Pre-commit Is Useful

Pre-commit hooks are valuable because they:

  • Catch issues early – Formatting errors, lint violations, and security problems are caught before code leaves your machine
  • Apply consistent rules – Remove most style nitpicks from code review with team-wide enforcement
  • Automate checks – Integrate naturally into the Git workflow instead of relying on people to remember separate commands
  • Self-healing – Many hooks can fix problems in place, so a failed hook often comes with an automatic patch to re-stage and commit

Choosing Hooks for a Python Project

For Python projects, you typically combine several focused tools:

Core Hooks (Fast & Essential)

  • Formatting: black or ruff format to enforce a consistent style
  • Linting: ruff or flake8 to catch common mistakes and style issues
  • Import ordering: isort (with --profile=black) to keep imports tidy
  • Housekeeping: trailing whitespace, end-of-file fixers, YAML/JSON checkers

Code Quality (Add as Projects Mature)

  • Type checking: mypy to statically check type hints and catch type-related bugs early
  • Docstrings: pydocstyle to enforce PEP 257 conventions; docformatter to auto-normalize
  • Security: bandit to detect insecure patterns in Python code
  • Dead code: vulture to find unused functions, classes, variables, and imports

Advanced Checks (Use When Needed)

  • Dependency security: pip-audit or safety to scan for known vulnerable packages
  • Code complexity: pylint for deeper semantic checks and code smell detection
  • Testing: pytest as a pre-commit or pre-push hook to ensure tests pass before changes land
  • Coverage: pytest-cov to block commits if coverage drops below a threshold
  • Branch protection: no-commit-to-branch to prevent accidental commits to main or release/*

!!! tip Start Small, Grow Smart Begin with a small, fast set of hooks and expand only when you see consistent value from additional checks. A fast feedback loop keeps developers happy.


Installing and Wiring Pre-commit

Info

To use the pre-commit framework in your project:

  1. Install the tool inside your project environment:

    pip install pre-commit
    

  2. Create a .pre-commit-config.yaml file in the repository root

  3. Register hooks with Git (run once per clone):

    pre-commit install
    

After this, every git commit will run the configured hooks on the staged files.

Running Hooks Manually

You can run all hooks on the whole codebase at any time:

pre-commit run --all-files

When to Use This

This is useful when adding pre-commit to an existing project or after changing the configuration to validate the entire codebase.


Example .pre-commit-config.yaml

Here is a minimal configuration that combines common Python tools:

.pre-commit-config.yaml
repos:
  - 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

  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: ["check"]

  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ["--profile=black"]

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

This configuration runs simple housekeeping hooks, formats code with Black, lints with Ruff, orders imports with isort using the Black profile, and runs Bandit for basic security checks.

Making Scripts and Hooks Work Together

Your bootstrap scripts and pre-commit configuration should complement each other:

Bootstrap installs dependencies – The script installs pre-commit and any tools referenced in .pre-commit-config.yaml, so hooks are guaranteed to have what they need

README stays simple – Describe onboarding in just a few lines, delegating details to scripts and configuration:

./scripts/bootstrap.sh   # or scripts/bootstrap.ps1 on Windows
pre-commit install
pytest

CI mirrors local workflow – Reuse these same scripts and pre-commit commands in CI to ensure local and remote behavior stay aligned

The Complete Picture

With this in place, quality checks become part of the default workflow instead of an optional extra step, and setting up the project on a new machine becomes repeatable, fast, and foolproof.