1. Pengenalan Pre-commit Hooks
Pre-commit hooks adalah script yang otomatis dijalankan sebelum kamu melakukan git commit. Tujuannya: mendeteksi dan memperbaiki masalah kode secara otomatis sebelum masuk ke repository. Ini menyelamatkan kamu dari code review yang memalukan karena typo, format yang berantakan, atau kode yang tidak aman.
Bayangkan kamu punya asisten pribadi yang selalu memeriksa setiap baris kode sebelum kamu commit — memastikan style konsisten, tidak ada bug sederhana, dan tidak ada secret yang bocor. Itulah pre-commit hooks!
Mengapa Pre-commit Hooks Penting?
| Manfaat | Penjelasan |
|---|---|
| Konsistensi kode | Seluruh tim menggunakan style yang sama |
| Detect bugs lebih awal | Error ditangkap sebelum masuk repository |
| Keamanan | Mencegah commit password, API keys, atau secret |
| Code review lebih cepat | Tidak perlu review masalah formatting/style |
| Otomatis | Tidak perlu diingatkan, hook berjalan sendiri |
| Standar tim | Semua aturan terdefinisi dalam satu file |
┌──────────┐ ┌──────────────────────────┐ ┌──────────────┐
│ Developer │ │ Pre-commit Hooks │ │ Git │
│ Tulis │───→│ │───→│ Repository │
│ Kode │ │ ✅ Lint check │ │ │
│ │ │ ✅ Format check │ │ (hanya │
│ │ │ ✅ Type check │ │ kode │
│ │ │ ✅ Security scan │ │ bersih) │
│ │ │ ✅ Test runners │ │ │
└──────────┘ └──────────────────────────┘ └──────────────┘
│
❌ Gagal → Commit diblokir, fix dulu
✅ Bersih → Commit berhasil
2. Setup & Instalasi
Menggunakan library pre-commit yang populer dan well-maintained. Setup sangat cepat.
# Instalasi pre-commit
pip install pre-commit
# Verifikasi instalasi
pre-commit --version
# Setup di project kamu
cd my-python-project
git init
# Buat konfigurasi (akan dibahas di section berikut)
# Atau langsung install hooks
pre-commit install
# Struktur project setelah setup
my-python-project/
├── .git/
├── .pre-commit-config.yaml ← Konfigurasi hooks
├── .pre-commit-hooks.yaml ← (opsional) Custom hook definitions
├── pyproject.toml ← Konfigurasi linter/formatter
├── src/
│ └── myproject/
│ └── __init__.py
└── tests/
Perintah Dasar pre-commit
| Perintah | Fungsi |
|---|---|
pre-commit install | Install git hook di repo lokal |
pre-commit run --all-files | Jalankan hooks di semua file |
pre-commit run <hook> | Jalankan hook spesifik saja |
pre-commit autoupdate | Update versi hook ke terbaru |
pre-commit clean | Hapus cache environments |
pre-commit uninstall | Hapus git hook |
# Jalankan hooks di semua file (pertama kali bisa lambat karena install environment)
pre-commit run --all-files
# Output contoh:
# black....................................................Passed
# isort....................................................Passed
# flake8...................................................Failed
# - myproject/utils.py:42:89: E501 line too long (120 > 88)
# Fix semua file otomatis
pre-commit run --all-files # hook yang auto-fix (black, isort) akan memperbaiki
# Jalankan hanya hook tertentu
pre-commit run black --all-files
pre-commit run flake8 --all-files
3. Konfigurasi .pre-commit-config.yaml
File .pre-commit-config.yaml adalah jantung dari setup pre-commit kamu. Di sini kamu mendefinisikan hooks mana yang akan dijalankan, versi apa, dan opsi apa yang digunakan.
# .pre-commit-config.yaml - Template Lengkap
repos:
# ========== FORMATTING ==========
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
language_version: python3.11
args: [--line-length=88]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: [--profile=black]
# ========== LINTING ==========
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args: [--max-line-length=88, --extend-ignore=E203,W503]
# ========== TYPE CHECKING ==========
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
# ========== SECURITY ==========
- repo: https://github.com/PyCQA/bandit
rev: 1.7.8
hooks:
- id: bandit
args: [-c, pyproject.toml]
# ========== MISC ==========
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
- id: debug-statements
- id: check-docstring-first
Konfigurasi Per-File & Exclude
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
# Exclude file tertentu
exclude: |
(?x)(
^migrations/|
^generated/|
\.pyi$
)
# Hanya jalankan di file Python tertentu
types: [python]
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
# File yang di-exclude
exclude: ^tests/fixtures/
args: [--config=.flake8]
# Tambahan dependency
additional_dependencies: [flake8-bugbear, flake8-comprehensions]
4. Linting: flake8, ruff, pylint
Linting adalah proses analisis kode untuk menemukan error, style violations, dan code smells. Berikut tiga linter populer untuk Python.
Flake8: Classic Python Linter
# .flake8 atau di setup.cfg
[flake8]
max-line-length = 88
extend-ignore = E203, W503
per-file-ignores =
__init__.py:F401
tests/*:E501
max-complexity = 10
exclude =
.git,
__pycache__,
build,
dist,
.venv,
migrations
# .pre-commit-config.yaml - flake8 dengan plugins
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear # Additional bug detection
- flake8-comprehensions # List/dict comprehension checks
- flake8-docstrings # Docstring conventions
- flake8-typing-imports # Import sorting for type hints
Ruff: Linter Super Cepat (Rekomendasi 2026)
# pyproject.toml - Ruff configuration
[tool.ruff]
target-version = "py311"
line-length = 88
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"S", # flake8-bandit (security)
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"RUF", # Ruff-specific rules
]
ignore = ["E501"]
[tool.ruff.per-file-ignores]
"tests/**" = ["S101"] # Allow assert in tests
"__init__.py" = ["F401"] # Allow unused imports
# .pre-commit-config.yaml - Ruff
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format # Ruff juga bisa format!
Ruff ditulis dalam Rust dan 10-100x lebih cepat dari flake8. Di tahun 2026, Ruff menjadi pilihan utama karena bisa menggantikan flake8 + isort + black sekaligus. Sangat direkomendasikan untuk project baru!
Pylint: Deep Analysis
# Pylint lebih berat tapi lebih mendalam
- repo: https://github.com/pylint-dev/pylint
rev: v3.1.0
hooks:
- id: pylint
args: [--disable=C0114,C0115,C0116] # Disable missing docstring
additional_dependencies: [pylint-flask, pylint-django]
5. Formatting: black, isort, ruff format
Formatter otomatis merapikan kode kamu sehingga seluruh project menggunakan style yang konsisten. Tidak perlu lagi debat tentang indentasi, panjang baris, atau quote style!
Black: The Uncompromising Formatter
# SEBELUM black:
def contoh_fungsi( nama, umur, alamat,kota ):
if nama and umur:
return {"nama": nama, "umur": umur,
"alamat": alamat,
"kota": kota}
return None
# SETELAH black:
def contoh_fungsi(nama, umur, alamat, kota):
if nama and umur:
return {
"nama": nama,
"umur": umur,
"alamat": alamat,
"kota": kota,
}
return None
# pyproject.toml - Black configuration
[tool.black]
line-length = 88
target-version = ["py311"]
include = '\.pyi?$'
extend-exclude = '''
/(
\.eggs
| \.git
| \.venv
| build
| dist
| migrations
)/
'''
isort: Import Sorting
# SEBELUM isort:
from myproject.utils import helper
import os
from datetime import datetime
import sys
from django.shortcuts import render
import json
from django.http import JsonResponse
# SETELAH isort:
import json
import os
import sys
from datetime import datetime
from django.http import JsonResponse
from django.shortcuts import render
from myproject.utils import helper
# pyproject.toml - isort configuration
[tool.isort]
profile = "black"
known_first_party = ["myproject"]
line_length = 88
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
# .pre-commit-config.yaml - Combo formatting
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: [--profile=black]
6. Security: bandit, safety
Bandit memindai kode Python untuk menemukan masalah keamanan yang umum — seperti penggunaan eval(), hardcoded password, insecure random, dan banyak lagi.
# Bandit akan mendeteksi masalah ini:
# B101: assert digunakan (berbahaya di production)
assert user.is_admin
# B301: pickle digunakan (bisa eksekusi arbitrary code)
import pickle
data = pickle.loads(user_input) # ⚠️ Bahaya!
# B303: MD5 digunakan (tidak aman)
import hashlib
hash = hashlib.md5(password.encode()) # ⚠️ Gunakan bcrypt!
# B311: random digunakan untuk keamanan
import random
token = random.randint(1000, 9999) # ⚠️ Gunakan secrets!
# B602: subprocess dengan shell=True
import subprocess
subprocess.call("ls -la", shell=True) # ⚠️ Gunakan list!
# B105: hardcoded password
password = "admin123" # ⚠️ Gunakan env vars!
# pyproject.toml - Bandit configuration
[tool.bandit]
exclude_dirs = ["tests"]
skips = ["B101"] # Skip assert check
# Atau dengan file .bandit
[bandit]
exclude = ./tests
skips = B101
# .pre-commit-config.yaml - Security hooks
- repo: https://github.com/PyCQA/bandit
rev: 1.7.8
hooks:
- id: bandit
args: [-c, pyproject.toml]
exclude: ^tests/
# Deteksi secret/credential yang tidak sengaja di-commit
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
Pre-commit hooks seperti gitleaks atau detect-secrets bisa mendeteksi API keys, password, dan token yang tidak sengaja masuk ke kode. Selalu gunakan .env file dan .gitignore untuk credentials.
7. Type Checking: mypy
mypy adalah static type checker untuk Python yang memverifikasi type annotations. Ini membantu menangkap bug yang berkaitan dengan tipe data sebelum runtime.
# Contoh kode yang akan di-check mypy
def hitung_luas(panjang: float, lebar: float) -> float:
return panjang * lebar
# ✅ Benar
hasil: float = hitung_luas(5.0, 3.0)
# ❌ Error: Incompatible types
hasil: str = hitung_luas(5.0, 3.0) # Error: float bukan str
# ❌ Error: Argument types
hitung_luas("5", 3) # Error: str bukan float
from typing import Optional
def cari_user(id: int) -> Optional[dict]:
if id > 0:
return {"id": id, "nama": "Budi"}
return None
# ❌ Tanpa check: crash saat akses .get()
user = cari_user(-1)
print(user.get("nama")) # AttributeError!
# ✅ Dengan check:
user = cari_user(-1)
if user is not None:
print(user.get("nama"))
# pyproject.toml - mypy configuration
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
show_error_codes = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
# .pre-commit-config.yaml - mypy hook
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args: [--strict]
additional_dependencies:
- types-requests
- types-PyYAML
- types-redis
8. Custom Hooks
Selain menggunakan hook yang sudah ada, kamu bisa membuat custom hooks untuk aturan spesifik project kamu. Misalnya: memastikan semua file punya header, tidak ada debug print, atau format commit message tertentu.
# .pre-commit-config.yaml - Custom local hooks
repos:
- repo: local
hooks:
# Hook 1: Cek tidak ada debug print
- id: no-debug-print
name: Check no debug print statements
entry: '^\s*print\('
language: pygrep
types: [python]
exclude: ^tests/
# Hook 2: Pastikan semua fungsi punya docstring
- id: check-docstrings
name: Check function docstrings
entry: python scripts/check_docstrings.py
language: system
types: [python]
pass_filenames: true
# Hook 3: Cek tidak ada TODO/FIXME di production
- id: no-todo
name: Check no TODO/FIXME
entry: 'TODO|FIXME|HACK|XXX'
language: pygrep
types: [python]
exclude: ^tests/
# Hook 4: Validasi commit message
- id: commit-msg-check
name: Validate commit message
entry: python scripts/validate_commit_msg.py
language: system
stages: [commit-msg]
# Hook 5: Jalankan pytest sebelum commit
- id: pytest-check
name: Run tests
entry: pytest tests/ -x -q
language: system
pass_filenames: false
always_run: true
Custom Hook Script: check_docstrings.py
#!/usr/bin/env python3
"""Pre-commit hook: Pastikan semua fungsi publik punya docstring."""
import ast
import sys
def check_file(filepath: str) -> list[str]:
errors = []
with open(filepath) as f:
tree = ast.parse(f.read(), filename=filepath)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Skip private functions
if node.name.startswith('_'):
continue
# Check docstring
if not (node.body and isinstance(node.body[0], ast.Expr)
and isinstance(node.body[0].value, ast.Constant)
and isinstance(node.body[0].value.value, str)):
errors.append(
f"{filepath}:{node.lineno} - "
f"Fungsi '{node.name}' tidak punya docstring"
)
return errors
if __name__ == "__main__":
all_errors = []
for filepath in sys.argv[1:]:
all_errors.extend(check_file(filepath))
if all_errors:
for error in all_errors:
print(f" ❌ {error}")
sys.exit(1)
sys.exit(0)
Validate Commit Message Hook
#!/usr/bin/env python3
"""Validasi format commit message (Conventional Commits)."""
import re
import sys
# Format: type(scope): description
PATTERN = r'^(feat|fix|docs|style|refactor|test|chore|perf|ci)(\(.+\))?: .{1,72}$'
def validate_message(msg_file: str) -> bool:
with open(msg_file) as f:
first_line = f.readline().strip()
if not re.match(PATTERN, first_line):
print(f"❌ Commit message tidak valid!")
print(f" Format: type(scope): description")
print(f" Contoh: feat(auth): tambah login OAuth")
print(f" Tipe: feat|fix|docs|style|refactor|test|chore|perf|ci")
print(f" Message: '{first_line}'")
return False
return True
if __name__ == "__main__":
if not validate_message(sys.argv[1]):
sys.exit(1)
print("✅ Commit message valid!")
sys.exit(0)
9. Integrasi CI/CD
Pre-commit hooks harus juga dijalankan di CI/CD pipeline untuk memastikan semua developer menjalankannya. Berikut integrasi dengan GitHub Actions.
# .github/workflows/pre-commit.yml
name: Pre-commit Checks
on:
pull_request:
push:
branches: [main, develop]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pre-commit
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install pre-commit
run: pip install pre-commit
- name: Run pre-commit
run: pre-commit run --all-files --show-diff-on-failure
GitHub Actions dengan Matrix Testing
# .github/workflows/ci.yml
name: CI Pipeline
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install pre-commit
- run: pre-commit run --all-files
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[dev]"
- run: pytest --cov=src --cov-report=xml
- uses: codecov/codecov-action@v4
with:
file: coverage.xml
Pre-commit.ci: Auto-Fix PRs
# Tambahkan di .pre-commit-config.yaml untuk auto-fix PR
ci:
autoupdate_schedule: weekly
autofix_commit_msg: 'style: auto-fix by pre-commit.ci'
autoupdate_commit_msg: 'chore: update pre-commit hooks'
Layanan pre-commit.ci bisa otomatis menjalankan hooks dan membuat PR perbaikan. Setup sangat mudah — cukup install app di GitHub repo kamu. Gratis untuk open source!
10. Best Practices & Troubleshooting
Kombinasi Hook yang Direkomendasikan
| Kategori | Hook | Alternatif |
|---|---|---|
| Format | black + isort | ruff format |
| Lint | ruff | flake8 + plugins |
| Type Check | mypy | pyright |
| Security | bandit + gitleaks | detect-secrets |
| Misc | trailing-whitespace, end-of-file-fixer | — |
Troubleshooting Umum
# Masalah: Hook lambat saat pertama kali
# Solusi: Gunakan --install-hooks untuk pre-install
pre-commit install-hooks
# Masalah: Ingin skip hook untuk commit darurat
# Solusi: --no-verify (gunakan dengan bijak!)
git commit -m "hotfix" --no-verify
# Masalah: Hook environment corrupt
# Solusi: Clean dan reinstall
pre-commit clean
pre-commit install-hooks
# Masalah: Update semua hook ke versi terbaru
pre-commit autoupdate
git add .pre-commit-config.yaml
git commit -m "chore: update pre-commit hooks"
# Masalah: Hook error karena Python version
# Solusi: Tentukan language_version di config
# hooks:
# - id: black
# language_version: python3.11
# Masalah: Cek apakah hook ter-install
pre-commit --version
ls .git/hooks/pre-commit
--no-verify melewati semua hooks. Ini berguna untuk emergency, tapi jangan jadikan kebiasaan. Jika hook sering gagal, perbaiki konfigurasi atau kodenya, jangan skip hooknya.
11. Quiz Pemahaman
1. Kapan pre-commit hooks dijalankan?
2. Perintah apa untuk menjalankan hooks di semua file yang sudah ada?
3. Tool mana yang bisa menggantikan black + isort + flake8 sekaligus?
4. Apa fungsi dari gitleaks dalam pre-commit hooks?
5. Bagaimana cara melewati hooks untuk commit darurat?