Understanding and using pre-commit hooks for code quality.
Related Documentation: Getting Started, Code Style
Pre-commit hooks automatically check and fix code quality issues before you commit. They ensure consistent code style, catch common errors, and enforce security best practices.
Pre-commit runs automatically when you git commit. It:
- Formats code - Applies black and isort
- Removes unused code - Cleans up imports and variables
- Checks for issues - Runs flake8 and bandit
- Fixes common problems - Trailing whitespace, line endings
- Prevents mistakes - Detects large files, merge conflicts, private keys
pip install pre-commit
pre-commit installExpected output:
pre-commit installed at .git/hooks/pre-commit
pre-commit --versionPre-commit runs automatically on git commit and includes a confirmation prompt:
git add .
git commit -m "Add new feature"Output:
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
black....................................................................Passed
isort....................................................................Passed
autoflake................................................................Passed
flake8 (warnings only)...................................................Passed
bandit (warnings only)...................................................Passed
╔════════════════════════════════════════════════════════════╗
║ Pre-Commit Check Summary ║
╚════════════════════════════════════════════════════════════╝
✓ All checks passed!
Do you want to proceed with the commit?
y or yes - Proceed with commit
n, no, q, or Ctrl+C - Cancel commit
Your choice: y
✓ Proceeding with commit...
Confirmation Prompt:
- After all hooks run, you'll see a summary
- Type
yoryesto proceed - Type
n,no,q, or pressCtrl+Cto cancel - 30-second timeout (auto-cancels if no response)
If flake8 or bandit find issues:
⚠ Flake8 (linting) found issues:
agencies/views.py:45:1: F401 'os' imported but unused
agencies/models.py:12:80: E501 line too long (92 > 88 characters)
⚠ Bandit (security) found issues:
>> Issue: [B105:hardcoded_password_string] Possible hardcoded password
Severity: Low Confidence: Medium
Location: config/settings.py:45
Do you want to proceed with the commit?
y or yes - Proceed with commit
n, no, q, or Ctrl+C - Cancel commit
Your choice: n
✗ Commit cancelled by user
What to do:
- Review the warnings carefully
- Decide if you want to fix them now or later
- Type
nto cancel and fix issues - Or type
yto proceed anyway (warnings are non-blocking)
Run hooks without committing:
# Run on all files
pre-commit run --all-files
# Run on staged files only
pre-commit run
# Run specific hook
pre-commit run black
# Run on specific files
pre-commit run --files agencies/models.py agencies/views.pyIf a hook fails, the commit is aborted:
git commit -m "Add feature"Output:
black....................................................................Failed
- hook id: black
- files were modified by this hook
reformatted agencies/views.py
1 file reformatted.
What to do:
- Review the changes made by the hook
- Stage the fixed files:
git add . - Commit again:
git commit -m "Add feature"
Sometimes you need to skip hooks (use sparingly):
# Skip all hooks
git commit --no-verify -m "WIP: temporary commit"
# Skip specific hook
SKIP=flake8 git commit -m "Fix urgent bug"
# Skip multiple hooks
SKIP=flake8,bandit git commit -m "Quick fix"When to skip:
- Work-in-progress commits on feature branches
- Urgent hotfixes (fix properly later)
- Known false positives
When NOT to skip:
- Commits to
mainordevelop - Pull requests
- Production releases
black - Formats Python code consistently:
- Enforces 88-character line length
- Standardises quotes, spacing, indentation
isort - Sorts and organises imports:
- Groups imports (standard library, third-party, local)
- Sorts alphabetically within groups
autoflake - Removes unused imports and variables:
- Removes unused imports
- Removes unused variables
flake8 (Warning-Only Mode) - Checks code for style and quality issues:
- Checks PEP 8 compliance
- Detects unused variables
- Reports warnings but doesn't block commits
bandit (Warning-Only Mode) - Checks for security issues:
- Detects SQL injection risks
- Finds hardcoded passwords
- Reports warnings but doesn't block commits
django-upgrade - Upgrades Django code to modern patterns:
- Updates deprecated Django APIs
- Modernises code for Django 5.1
Commit Confirmation - Provides summary and confirmation prompt:
- Runs after all other hooks complete
- Displays summary of any issues found
- Prompts for confirmation before proceeding
Other file checks:
- trailing-whitespace - Removes trailing whitespace
- end-of-file-fixer - Ensures files end with newline
- check-yaml - Validates YAML syntax
- check-json - Validates JSON syntax
- check-toml - Validates TOML syntax
- check-added-large-files - Prevents committing large files (>1MB)
- check-merge-conflict - Detects unresolved merge conflict markers
- check-case-conflict - Detects files that differ only in case
- detect-private-key - Prevents committing private keys
- mixed-line-ending - Ensures consistent line endings (LF)
Main configuration file defining all hooks.
Location: backend/.pre-commit-config.yaml
Key settings:
default_language_version:
python: python3.13
fail_fast: false # Run all hooks even if one fails
exclude: | # Files to skip
migrations/.*
__pycache__/.*
\.venv/.*.pre-commit-confirm-wrapper.sh - Provides final confirmation prompt:
- Runs flake8 and bandit
- Shows coloured output
- Prompts for user confirmation
- 30-second timeout
.pre-commit-flake8-wrapper.sh - Makes flake8 non-blocking:
- Shows warnings without blocking commits
.pre-commit-bandit-wrapper.sh - Makes bandit non-blocking:
- Shows security warnings without blocking commits
Configuration for black, isort, and bandit:
[tool.black]
line-length = 88
target-version = ['py313']
[tool.isort]
profile = "black"
line_length = 88
[tool.bandit]
exclude_dirs = ["tests", "migrations"]
skips = ["B101"] # Skip assert_used checkSolution:
pre-commit installSolution:
poetry --version
poetry install
pre-commit run --all-filesSolution: Configuration already handles this:
- Both use 88-character line length
- flake8 ignores E203, W503 (black-incompatible rules)
Solution:
# Add inline comment to suppress
password = get_password_from_env() # nosec B105
# Or skip the check globally in pyproject.toml
[tool.bandit]
skips = ["B105"]Solution:
git add .
git commit -m "Your message"Solution:
pre-commit autoupdate
pre-commit run --all-files
git add .pre-commit-config.yaml
git commit -m "Update pre-commit hooks"pre-commit run --all-filesDon't skip hooks to avoid fixing issues. Address them as you go:
pre-commit run --all-files
# Fix reported issues
git add .
git commit -m "Fix code quality issues"Always review changes made by hooks:
git diff
git add -p # Interactive staging# Update hook versions quarterly
pre-commit autoupdate
pre-commit run --all-files
git add .pre-commit-config.yaml
git commit -m "Update pre-commit hooks"Pre-commit hooks run locally, but CI/CD also enforces code quality:
Local (pre-commit):
- Fast feedback
- Catches issues before push
- Auto-fixes formatting
CI/CD (GitHub Actions):
- Enforces on all PRs
- Runs full test suite
- Generates coverage reports
- Code Style - Detailed code style guidelines
- Testing Guide - Testing strategy