Pre-commit Hooks¶
Pre-commit hooks automate code quality checks before commits enter version control. This guide covers comprehensive hook configuration for Python and Django projects using Ruff, Bandit, Vulture, djhtml, and other essential tools.
Why Pre-commit Hooks Matter¶
Automated Quality Gates
Pre-commit hooks catch issues before they enter version control, saving time in code review and preventing broken code from reaching production.
Key Benefits: - Catch issues early - Before code review, before CI - Consistent formatting - Automatic code formatting - Security scanning - Detect vulnerabilities automatically - Fast feedback - Instant feedback while coding - Team consistency - Same checks for all developers - Reduced review time - Reviewers focus on logic, not style
Pre-commit Framework¶
Installation¶
Install pre-commit framework globally or per-project:
# With uv (recommended)
uv pip install pre-commit
# Or with pip
pip install pre-commit
# Or via package manager
brew install pre-commit # macOS
apt install pre-commit # Ubuntu/Debian
Basic Setup¶
# Initialize in your repository
cd your-project
pre-commit install
# Output:
# pre-commit installed at .git/hooks/pre-commit
This creates a Git hook that runs before each commit.
Configuration File¶
Create .pre-commit-config.yaml in repository root:
# Minimum viable configuration
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
Test configuration:
Essential Pre-commit Hooks¶
Standard Pre-commit Hooks¶
The pre-commit-hooks repository provides fundamental checks:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
# File formatting
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-byte-order-marker
# File conflicts
- id: check-case-conflict
- id: check-merge-conflict
# File validation
- id: check-json
- id: check-toml
- id: check-yaml
# Python specific
- id: check-docstring-first
- id: debug-statements
- id: name-tests-test
args: ['--django']
# Security
- id: detect-private-key
# Dependencies
- id: requirements-txt-fixer
Hook Descriptions:
trailing-whitespace: Removes trailing whitespaceend-of-file-fixer: Ensures files end with newlinecheck-yaml: Validates YAML syntaxcheck-json: Validates JSON syntaxcheck-toml: Validates TOML syntaxcheck-docstring-first: Ensures docstring before codedebug-statements: Catchespdb,breakpoint()callsdetect-private-key: Finds private keys in codename-tests-test: Ensures test files named correctly
Excluding Files and Directories¶
Global exclusions apply to all hooks:
Pattern Syntax:
- (?x) enables verbose regex mode
- ^ matches start of path
- | separates multiple patterns
- Trailing $ matches end
Per-hook exclusions:
Ruff: Formatting and Linting¶
Ruff combines formatting (Black replacement) and linting (Flake8/isort replacement) in one fast tool:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
# Linting with auto-fix
- id: ruff
args:
- --unsafe-fixes
- --fix
- --select=T201,T203,W191,W291,W292,W605,LOG001,LOG002,LOG007,LOG009,PLC0206,PLC0208,Q000,Q001,Q002,Q003
# Formatting
- id: ruff-format
Ruff Linting Rules¶
Selected Rules Explained:
args:
- --unsafe-fixes # Apply potentially risky fixes
- --fix # Auto-fix issues
- --select=... # Specific rules to enforce
Rule Categories:
T201, T203: Disallowprint()andpprint()in production codeW191: No tabs (use spaces)W291, W292: No trailing whitespaceW605: Invalid escape sequencesLOG001, LOG002, LOG007, LOG009: Logging best practicesPLC0206, PLC0208: Dictionary/list comprehension issuesQ000, Q001, Q002, Q003: Quote consistency
Why These Specific Rules:
Most formatting and style rules are handled by ruff format. The linter focuses on:
- Code that might cause bugs (escape sequences)
- Production code quality (no print statements)
- Logging best practices
- Consistency (quotes, whitespace)
Ruff Configuration¶
Extend configuration in pyproject.toml:
[tool.ruff]
line-length = 180
target-version = "py313"
exclude = [
".git",
".venv",
"__pycache__",
"migrations",
"node_modules",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
]
ignore = [
"E501", # Line too long (handled by formatter)
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # Allow assert in tests
]
Bandit: Security Scanning¶
Bandit scans Python code for common security issues:
- repo: https://github.com/PyCQA/bandit
rev: 1.8.0
hooks:
- id: bandit
language_version: python3
args:
- -l # Severity level threshold (low)
- --recursive # Scan subdirectories
- -x # Exclude tests
- tests
- -q # Quiet mode (errors only)
- -s # Skip specific tests
- B310,B410,B608,B101,B314,B404
files: \.py$
Bandit Skipped Tests¶
Skipped Tests Explained:
B310: Audit URL open for permitted schemes-
Why skip: Django uses URLs extensively, false positives
-
B410: Import XML modules -
Why skip: Django templates use XML, known safe contexts
-
B608: SQL injection (hardcoded SQL) -
Why skip: Django ORM prevents this, false positives on raw strings
-
B101: Assert used -
Why skip: Valid in tests, Python optimizes away in production
-
B314: XML parsing (blacklist) -
Why skip: Django uses safe XML parsing
-
B404: Import subprocess module - Why skip: Sometimes necessary, review manually
Bandit Configuration¶
Create .bandit configuration file:
# .bandit
exclude_dirs:
- /tests/
- /migrations/
- /node_modules/
- /.venv/
skips:
- B310 # Audit URL open
- B410 # Import XML
- B608 # SQL injection check
- B101 # Assert usage
- B314 # XML parsing
- B404 # Subprocess import
tests:
- B201 # Flask debug mode
- B501 # Request without cert validation
- B502 # SSL with bad version
- B503 # SSL with bad defaults
- B506 # Unsafe YAML load
- B601 # Paramiko exec/shell
- B602 # Shell=True subprocess
Inline Bandit Suppressions¶
For legitimate security exceptions:
# Suppress specific warning with comment
import subprocess # nosec B404
# Execute shell command (reviewed and safe)
result = subprocess.run(["ls", "-la"], shell=False) # nosec B602
# Hardcoded test password (only in tests)
TEST_PASSWORD = "test123" # nosec B105
Use Suppressions Sparingly
Only suppress Bandit warnings after thorough security review. Document why the suppression is safe.
Vulture: Dead Code Detection¶
Vulture finds unused code:
- repo: local
hooks:
- id: vulture
name: vulture
entry: vulture .vulture.py --min-confidence 100 --exclude "venv,node_modules,migrations,management,fixtures/cassettes,.md,README.md,TEST_DATABASE_README.md,TEST_DATABASE_SETUP.md,conftest_playwright.py"
language: system
pass_filenames: false
files: ^poseidon/tests/
Vulture Configuration¶
Create .vulture.py whitelist for false positives:
# .vulture.py
"""Vulture whitelist for false positives."""
# Django model fields accessed via ORM
_.objects # Django Manager
_.DoesNotExist # Django exception
_.MultipleObjectsReturned # Django exception
# Django methods
_.get_absolute_url
_.get_queryset
_.get_context_data
_.form_valid
_.form_invalid
# Django class attributes
_.verbose_name
_.verbose_name_plural
_.ordering
# pytest fixtures
_.pytest_configure
_.pytest_sessionstart
_.pytest_collection_modifyitems
# Django settings
_.INSTALLED_APPS
_.MIDDLEWARE
_.TEMPLATES
# Django URL patterns
_.urlpatterns
_.app_name
# Django admin
_.list_display
_.list_filter
_.search_fields
_.readonly_fields
# DRF ViewSets
_.permission_classes
_.serializer_class
_.queryset
_.filter_backends
# Celery tasks
_.delay
_.apply_async
Running Vulture¶
# Manual run with detailed output
vulture myproject/ --min-confidence 80
# Show only high-confidence findings
vulture myproject/ --min-confidence 100
# Use whitelist
vulture myproject/ .vulture.py --min-confidence 80
Confidence Levels:
- 100%: Definitely unused
- 80-99%: Probably unused
- 60-79%: Maybe unused
Pre-commit Recommendation: Use --min-confidence 100 to avoid false positives.
Pylint: Advanced Linting¶
Pylint provides comprehensive code analysis:
- repo: local
hooks:
- id: pylint
name: pylint
entry: python -u -m pylint
language: system
types: [python]
pass_filenames: true
verbose: false
args:
- --jobs=20
- --persistent=no
- --output-format=text
- --rcfile=.pylintrc
files: ^poseidon/tests/
exclude: ^poseidon/tests/conftest\.py$
Pylint Configuration¶
Create .pylintrc:
[MASTER]
jobs=4
ignore=migrations,node_modules,.venv,venv
persistent=no
[MESSAGES CONTROL]
disable=
C0111, # missing-docstring
C0103, # invalid-name (conflicts with Django conventions)
R0903, # too-few-public-methods (Django models)
R0801, # duplicate-code (tests often similar)
W0212, # protected-access (needed for Django internals)
W0613, # unused-argument (Django view signatures)
[FORMAT]
max-line-length=180
indent-string=' '
[DESIGN]
max-args=10
max-locals=25
max-returns=10
max-branches=20
max-statements=75
[SIMILARITIES]
min-similarity-lines=10
ignore-comments=yes
ignore-docstrings=yes
Django-Specific Disables:
C0103: Django usespk,id(not PEP 8 compliant)R0903: Django models often have few public methodsW0212: Accessing_metaand other protected attributes is commonW0613: Django views receiverequesteven if unused
djhtml: Django Template Formatting¶
Format Django/Jinja templates:
- repo: https://github.com/rtts/djhtml
rev: '3.0.7'
hooks:
- id: djhtml
files: \.*/templates/.*\.html$
Features:
- Indents Django template tags
- Formats HTML consistently
- Handles {% block %}, {% if %}, etc.
- Preserves template logic
Example:
<!-- Before djhtml -->
{% if user.is_authenticated %}
<div class="welcome">
<h1>Welcome, {{ user.username }}!</h1>
</div>
{% endif %}
<!-- After djhtml -->
{% if user.is_authenticated %}
<div class="welcome">
<h1>Welcome, {{ user.username }}!</h1>
</div>
{% endif %}
pyupgrade: Modern Python Syntax¶
Automatically upgrade to modern Python syntax:
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
hooks:
- id: pyupgrade
args: [--py312-plus]
Transformations:
# Before pyupgrade
from typing import List, Dict, Optional
def process(items: List[str]) -> Dict[str, int]:
result: Dict[str, int] = {}
for item in items:
result[item] = len(item)
return result
# After pyupgrade (Python 3.12+)
def process(items: list[str]) -> dict[str, int]:
result: dict[str, int] = {}
for item in items:
result[item] = len(item)
return result
Other Upgrades:
- dict() → {}
- set([]) → set()
- .format() → f-strings (where safe)
- Old-style % formatting → f-strings
- typing.List → list (Python 3.9+)
Custom Local Hooks¶
Create project-specific hooks:
- repo: local
hooks:
- id: check-gunicorn
name: Check if Gunicorn is installed
entry: python .hooks/check_gunicorn.py
language: system
types: [python]
files: ''
pass_filenames: false
Custom Hook Script (.hooks/check_gunicorn.py):
#!/usr/bin/env python
"""Check if Gunicorn is properly installed in requirements."""
import sys
from pathlib import Path
def check_gunicorn():
"""Ensure Gunicorn is in requirements for production."""
req_file = Path("requirements/production.txt")
if not req_file.exists():
print(f"Warning: {req_file} not found")
return 0
content = req_file.read_text()
if "gunicorn" not in content.lower():
print(f"Error: Gunicorn not found in {req_file}")
print("Add 'gunicorn>=22.0.0' to production requirements")
return 1
return 0
if __name__ == "__main__":
sys.exit(check_gunicorn())
Other Custom Hook Ideas:
- repo: local
hooks:
# Check for Django migrations
- id: check-migrations
name: Check for pending migrations
entry: python manage.py makemigrations --check --dry-run
language: system
pass_filenames: false
files: models\.py$
# Validate Django settings
- id: check-django
name: Django system check
entry: python manage.py check --deploy
language: system
pass_filenames: false
# Security check
- id: check-security
name: Django security check
entry: python manage.py check --deploy --fail-level WARNING
language: system
pass_filenames: false
Complete Configuration Example¶
Production-ready .pre-commit-config.yaml:
# Python & Django Pre-commit Configuration
# Comprehensive hooks for code quality, security, and consistency
default_language_version:
python: python3.13
default_stages: [pre-commit, pre-push]
exclude: >
(?x)^(
migrations/|
htmlcov/|
node_modules/|
\.venv/|
build/|
dist/
)$
repos:
# Standard pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
# File formatting
- id: check-added-large-files
- id: check-byte-order-marker
- id: check-case-conflict
- id: trailing-whitespace
- id: end-of-file-fixer
# File validation
- id: check-json
- id: check-toml
- id: check-yaml
# Python specific
- id: check-docstring-first
- id: debug-statements
- id: name-tests-test
args: ['--django']
exclude: ^tests/conftest\.py$
# Security
- id: detect-private-key
exclude: ^\.localstack/production-ssm-export\.json$
# Dependencies
- id: requirements-txt-fixer
# Merge conflicts
- id: check-merge-conflict
# Security scanning
- repo: https://github.com/PyCQA/bandit
rev: 1.8.0
hooks:
- id: bandit
language_version: python3
args:
- -l
- --recursive
- -x
- tests
- -q
- -s
- B310,B410,B608,B101,B314,B404
files: \.py$
# Advanced linting
- repo: local
hooks:
- id: pylint
name: pylint
entry: python -u -m pylint
language: system
types: [python]
pass_filenames: true
verbose: false
args:
- --jobs=20
- --persistent=no
- --output-format=text
- --rcfile=.pylintrc
files: ^tests/
exclude: ^tests/conftest\.py$
# Dead code detection
- repo: local
hooks:
- id: vulture
name: vulture
entry: vulture .vulture.py --min-confidence 100 --exclude "venv,node_modules,migrations,management"
language: system
pass_filenames: false
files: ^src/
# Ruff linting (fast, comprehensive)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
args:
- --unsafe-fixes
- --fix
- --select=T201,T203,W191,W291,W292,W605,LOG001,LOG002,LOG007,LOG009,PLC0206,PLC0208,Q000,Q001,Q002,Q003
# Ruff formatting (Black replacement)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff-format
# Django template formatting
- repo: https://github.com/rtts/djhtml
rev: '3.0.7'
hooks:
- id: djhtml
files: \.*/templates/.*\.html$
# Modern Python syntax
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
hooks:
- id: pyupgrade
args: [--py312-plus]
# Custom project hooks
- repo: local
hooks:
- id: check-django
name: Django system check
entry: python manage.py check
language: system
pass_filenames: false
files: \.py$
ci:
autoupdate_schedule: weekly
skip: []
submodules: false
Hook Execution Stages¶
Configure when hooks run:
default_stages: [pre-commit, pre-push]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
# Run on every commit
- id: trailing-whitespace
stages: [pre-commit]
# Run only before push
- id: check-added-large-files
stages: [pre-push]
# Run manually only
- id: check-yaml
stages: [manual]
Available Stages:
- pre-commit: Before git commit
- pre-push: Before git push
- pre-merge-commit: Before merge commit
- pre-rebase: Before rebase
- manual: Only when run manually
Install Hooks for Stages:
# Install all stages
pre-commit install --install-hooks --hook-type pre-commit
pre-commit install --hook-type pre-push
pre-commit install --hook-type pre-merge-commit
# Or all at once
pre-commit install --install-hooks -t pre-commit -t pre-push
Running Pre-commit Hooks¶
Automatic Runs¶
After pre-commit install, hooks run automatically:
# Commit triggers pre-commit hooks
git add .
git commit -m "feat: add new feature"
# Output:
# Trim Trailing Whitespace.......Passed
# Fix End of Files...............Passed
# Check Yaml.....................Passed
# ruff.........................Passed
# ruff-format..................Passed
If hooks fail, commit is aborted:
git commit -m "fix: bug fix"
# Output:
# Trim Trailing Whitespace.......Failed
# - hook id: trailing-whitespace
# - files were modified by this hook
#
# Fixing myfile.py
# Fix issues and retry
git add .
git commit -m "fix: bug fix"
Manual Runs¶
Run hooks manually anytime:
# Run all hooks on all files
pre-commit run --all-files
# Run all hooks on staged files
pre-commit run
# Run specific hook
pre-commit run ruff --all-files
pre-commit run bandit --all-files
# Run on specific files
pre-commit run --files myapp/models.py myapp/views.py
Skipping Hooks¶
Skip hooks when necessary (use sparingly):
# Skip all hooks for one commit
git commit -m "fix: urgent hotfix" --no-verify
# Skip specific hook
SKIP=pylint git commit -m "fix: temp commit"
# Skip multiple hooks
SKIP=pylint,bandit git commit -m "WIP: work in progress"
Use --no-verify Sparingly
Skipping hooks should be rare and only for emergencies. The hooks exist to maintain code quality and security.
Updating Hooks¶
Manual Updates¶
Update hook versions:
# Update all hooks to latest versions
pre-commit autoupdate
# Update specific repository
pre-commit autoupdate --repo https://github.com/astral-sh/ruff-pre-commit
Output:
Updating https://github.com/pre-commit/pre-commit-hooks ... updating v4.5.0 -> v5.0.0.
Updating https://github.com/astral-sh/ruff-pre-commit ... updating v0.11.0 -> v0.11.8.
CI Auto-updates¶
Configure automatic updates in .pre-commit-config.yaml:
ci:
autoupdate_schedule: weekly # or monthly, quarterly
autoupdate_branch: 'main'
skip: [] # Hooks to skip in CI
submodules: false
Pre-commit.ci service (if enabled) will create PRs with updates.
Troubleshooting¶
Hook Failures¶
Common Issues:
# Hook not found
# Solution: Install hooks
pre-commit install
# Python not found
# Solution: Ensure Python in PATH
which python3
# Hook fails on specific file
# Solution: Exclude file
# In .pre-commit-config.yaml:
exclude: problematic_file\.py$
# Hook takes too long
# Solution: Limit files
files: ^src/ # Only run on src/ directory
Performance Optimization¶
# Speed up hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
# Limit to changed files only (default)
pass_filenames: true
# Or run on whole codebase
pass_filenames: false
args: [--all-files]
Performance Tips:
- Use fast tools: Ruff (Rust) > Pylint (Python)
- Limit scope: Only run on changed files
- Parallel execution: Pre-commit runs hooks in parallel
- Skip slow hooks: Move slow checks to pre-push or CI
Debugging Hooks¶
# Verbose output
pre-commit run --verbose --all-files
# Show which hooks will run
pre-commit run --show-diff-on-failure
# Run in debug mode
pre-commit run --all-files --verbose --debug
CI/CD Integration¶
GitHub Actions¶
Run pre-commit in CI:
# .github/workflows/pre-commit.yml
name: Pre-commit
on:
pull_request:
push:
branches: [main, develop]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- uses: pre-commit/action@v3.0.1
GitLab CI¶
# .gitlab-ci.yml
pre-commit:
image: python:3.13-slim
stage: test
before_script:
- pip install pre-commit
script:
- pre-commit run --all-files
cache:
paths:
- ~/.cache/pre-commit
Pre-commit.ci Service¶
Free service for open-source projects:
- Visit pre-commit.ci
- Enable for your repository
- Configure auto-fix PRs
Benefits: - Automatic hook runs on PRs - Auto-fix commits - Auto-update hook versions - Free for open source
Team Adoption¶
Initial Setup¶
# 1. Install pre-commit framework
pip install pre-commit
# 2. Create configuration
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
EOF
# 3. Install hooks
pre-commit install
# 4. Run on all files to establish baseline
pre-commit run --all-files
# 5. Commit configuration
git add .pre-commit-config.yaml
git commit -m "chore: add pre-commit hooks"
Team Onboarding¶
Document in CONTRIBUTING.md:
## Pre-commit Hooks
This project uses pre-commit hooks to maintain code quality.
### Setup
```bash
# Install pre-commit framework
pip install pre-commit
# Install hooks
pre-commit install
# Run manually
pre-commit run --all-files
What Hooks Do¶
- Ruff format: Formats Python code (Black-compatible)
- Ruff lint: Checks code style and quality
- Bandit: Scans for security vulnerabilities
- djhtml: Formats Django templates
- pyupgrade: Upgrades to modern Python syntax
Dealing with Failures¶
If a hook fails:
- Auto-fixed: Add fixed files and commit again
- Manual fix: Fix the issue and commit again
- False positive: Suppress or exclude (discuss with team)
Skipping Hooks (Emergency Only)¶
Only skip hooks for emergencies!
### Gradual Rollout
For existing projects with many violations:
```bash
# 1. Start with just formatting
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff-format
EOF
# 2. Format entire codebase
ruff format .
git add .
git commit -m "style: format codebase with Ruff"
# 3. Add more hooks incrementally
# Week 2: Add basic checks
# Week 3: Add linting
# Week 4: Add security scanning
Best Practices¶
✅ Do¶
- Install hooks immediately - First thing in new project
- Run on all files initially - Establish baseline
- Keep hooks fast - < 10 seconds for pre-commit
- Auto-fix when possible - Reduce manual work
- Document suppressions - Explain why issues are ignored
- Update regularly - Use
pre-commit autoupdate - Run in CI - Catch missed checks
- Share configuration - Commit
.pre-commit-config.yaml
❌ Don't¶
- Don't skip hooks routinely
- Don't add too many slow hooks to pre-commit
- Don't suppress warnings without understanding them
- Don't install hooks without team agreement
- Don't use
--no-verifyexcept emergencies - Don't add hooks that require manual intervention
- Don't forget to install hooks after cloning
Hook Selection Guidelines¶
Pre-commit Stage (fast, automatic): - Code formatting (Ruff format) - Basic linting (Ruff) - File validation (YAML, JSON) - Trailing whitespace, EOF - Debug statement detection
Pre-push Stage (slower, less frequent): - Comprehensive linting (Pylint) - Security scanning (Bandit) - Dead code detection (Vulture) - Type checking (mypy) - Django system checks
CI Only (slowest, most comprehensive): - Full test suite - Coverage reports - Integration tests - Build verification - Deployment checks
Advanced Configurations¶
Conditional Hooks¶
Run hooks based on conditions:
repos:
- repo: local
hooks:
# Only run in production branch
- id: production-checks
name: Production readiness checks
entry: bash -c 'if [ "$PRE_COMMIT_FROM_REF" = "refs/heads/main" ]; then python manage.py check --deploy; fi'
language: system
pass_filenames: false
Multi-language Hooks¶
For projects with Python and JavaScript:
repos:
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff-format
- id: ruff
# JavaScript hooks
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.0.0
hooks:
- id: eslint
files: \.[jt]sx?$
types: [file]
# CSS hooks
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
files: \.(css|scss|html)$
Environment-Specific Hooks¶
repos:
- repo: local
hooks:
- id: check-env
name: Check environment variables
entry: python .hooks/check_env.py
language: system
pass_filenames: false
Custom check script (.hooks/check_env.py):
#!/usr/bin/env python
"""Check required environment variables are documented."""
import os
import sys
from pathlib import Path
REQUIRED_VARS = [
"DATABASE_URL",
"SECRET_KEY",
"REDIS_URL",
]
def check_env_example():
"""Ensure .env.example contains all required variables."""
env_example = Path(".env.example")
if not env_example.exists():
print("Error: .env.example not found")
return 1
content = env_example.read_text()
missing = [var for var in REQUIRED_VARS if var not in content]
if missing:
print(f"Error: Missing variables in .env.example: {missing}")
return 1
print("✓ All required variables documented in .env.example")
return 0
if __name__ == "__main__":
sys.exit(check_env_example())
Integration with justfile¶
Combine pre-commit with justfile commands:
# Run pre-commit hooks
pc:
pre-commit run --all-files
# Run pre-commit on specific hook
pch HOOK:
pre-commit run {{HOOK}} --all-files
# Update pre-commit hooks
pcu:
pre-commit autoupdate
# Install pre-commit hooks
pci:
pre-commit install --install-hooks -t pre-commit -t pre-push
# Run all quality checks
check: pc
pytest --cov=myapp
python manage.py check --deploy
Usage:
# Run all hooks
just pc
# Run specific hook
just pch ruff
# Update hooks
just pcu
# Full quality check
just check
Summary¶
Pre-commit hooks automate code quality and security checks, catching issues before they enter version control. Key takeaways:
- Ruff provides fast formatting and linting (replaces Black + Flake8)
- Bandit scans for security vulnerabilities
- Vulture detects unused code
- Pylint offers comprehensive code analysis
- djhtml formats Django templates
- pyupgrade modernizes Python syntax
- Custom hooks enforce project-specific requirements
Essential Configuration: 1. Start with Ruff (format + lint) 2. Add security scanning (Bandit) 3. Include Django-specific checks 4. Customize for your workflow 5. Run in CI for enforcement
Well-configured pre-commit hooks maintain consistent code quality with minimal developer effort.
Next Steps¶
- Git Workflow - Commit conventions and PR process
- Gitignore Configuration - File exclusion patterns
- Ruff Linting - Comprehensive linting guide
- Security Best Practices - Security guidelines
- CI/CD Overview - Continuous integration setup