Cuando trabajas en equipo (o incluso solo), hay una brecha dolorosa entre el momento en que escribes código y el momento en que CI te dice que algo está mal. Puede ser media hora después, puede ser un pipeline de 10 minutos que falla en el primer check de formato. Pre-commit cierra esa brecha ejecutando validaciones automáticamente en tu máquina antes de que el commit llegue a existir.
La idea es precisa: pre-commit es un framework que gestiona hooks de Git — scripts que Git invoca en momentos específicos del ciclo de vida de un repositorio. El hook pre-commit de Git se dispara justo antes de crear el objeto commit; si cualquier hook sale con código distinto de cero, el commit se aborta. Lo que hace el framework es darte una capa de gestión encima: versiones fijadas de herramientas, aislamiento en entornos virtuales propios, configuración declarativa en un solo archivo YAML.
¿Por qué necesitas el framework y no simplemente scripts de shell en .git/hooks/? Porque .git/ no se versiona. Cada desarrollador tendría que configurar sus hooks manualmente, y las versiones divergirían en días. Pre-commit resuelve eso: la configuración va en .pre-commit-config.yaml, se versiona con el código, y cualquier colaborador la activa con un solo comando.
El momento de usarlo es desde el primer día de un proyecto profesional. No después de acumular deuda técnica. Los hooks más útiles en Python son los de siempre: ruff para linting (y opcionalmente formateo), mypy para verificación de tipos estáticos, y detección de secretos hardcodeados. Si dejas que un secret llegue a CI, ya está en el historial de Git.
Lo que se rompe si no tienes esto: el CI se convierte en un árbitro lento de errores triviales. Un desarrollador pushea, espera 8 minutos, descubre que hay trailing whitespace en un archivo de tests, corrige, pushea de nuevo. Multiplicado por el equipo, por semanas, es ruido puro. Los pre-commit hooks no reemplazan el CI — no tienen acceso a la base de datos de staging, no corren los tests de integración, no construyen el artefacto final — pero filtran todo lo que sí se puede verificar localmente en segundos.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
args: [--fix] # corrige auto lo que puede; falla si queda algo sin corregir
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-requests] # stubs de terceros que mypy necesita
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer # garantiza newline al final de cada archivo
- id: check-yaml
- id: detect-private-key # busca patrones de claves privadas RSA/SSH en el diff
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: [--baseline, .secrets.baseline] # archivo de falsos positivos conocidos
# Una sola vez por clon del repositorio pip install pre-commit pre-commit install # instala el hook en .git/hooks/pre-commit # Ejecutar manualmente contra todos los archivos (útil en CI o para primer setup) pre-commit run --all-files
# src/payments/processor.py — archivo de ejemplo que los hooks analizarán
from __future__ import annotations
import os
from decimal import Decimal
def charge(amount: Decimal, currency: str) -> dict[str, object]:
"""Procesa un cargo y devuelve la respuesta del gateway."""
api_key = os.environ["PAYMENT_API_KEY"] # correcto: no hardcodeado
if amount <= Decimal("0"):
raise ValueError(f"amount must be positive, got {amount}")
return {"status": "ok", "amount": str(amount), "currency": currency}
Qué hace cada decisión en este setup
rev fijada en cada repo es lo más importante del YAML. Si usas main o latest, tu hook de hoy puede producir un resultado diferente al de mañana. Las revisiones fijas garantizan que el mismo commit pasa los mismos checks en tu máquina, en la de tu colega, y en CI. Cuando quieras actualizar, ejecutas pre-commit autoupdate y obtienes un diff limpio en el YAML que puedes revisar y commitear.
ruff con --fix tiene una semántica interesante: intenta reparar lo que puede automáticamente (imports no usados, expresiones simplificables) y luego falla si el archivo modificado ya no está en el staging area. Git ve que el archivo cambió pero el nuevo contenido no está staged, así que el commit se aborta. Tú haces git add -p para revisar las correcciones y repites. Eso es deliberado: no quieres que una herramienta commitee código en tu nombre sin que lo hayas visto.
mypy con additional_dependencies resuelve un problema frecuente: mypy corre en un virtualenv aislado que pre-commit gestiona internamente, separado de tu virtualenv de proyecto. Si tu código importa requests y mypy necesita types-requests para inferir tipos, tienes que declararlo ahí. Si no lo haces, mypy lanza error: Skipping analyzing "requests" y el hook falla de forma confusa.
detect-secrets con baseline permite convivir con falsos positivos. Si tienes un string que parece una clave API pero es un ejemplo en la documentación, lo registras en el baseline y el hook lo ignora en adelante. Sin baseline, el hook se vuelve tan ruidoso que la gente lo desactiva.
El flujo completo desde esta configuración es: escribes código → git commit dispara pre-commit → ruff formatea y linta, mypy verifica tipos, los hooks básicos limpian trailing whitespace → si todo pasa, el commit existe → git push → CI corre exactamente pre-commit run --all-files más los tests de integración. La consistencia local/CI viene de usar la misma herramienta con el mismo YAML; no hay dos fuentes de verdad.
Errores que debes conocer
Error: Instalar pre-commit en el virtualenv del proyecto y olvidar ejecutar pre-commit install en nuevos clones, dejando el repositorio sin hooks activos.
# ❌ Wrong — pre-commit está instalado pero el hook no está registrado en .git/ pip install pre-commit # ... el desarrollador commitea sin ninguna validación # ✅ Right — instala Y registra pip install pre-commit pre-commit install
Añade pre-commit install a tu Makefile, justfile, o script de setup del repositorio para que sea imposible olvidarlo.
Error: Declarar mypy como hook pero no sincronizar su configuración con mypy.ini o pyproject.toml, haciendo que el hook use opciones diferentes a las del CI.
# ❌ Wrong — mypy.ini existe pero el hook lo ignora porque no lo encuentra # en .pre-commit-config.yaml no hay args, y mypy.ini está en un subdirectorio # ✅ Right — en pyproject.toml, en la raíz del repo [tool.mypy] strict = true python_version = "3.12"
Mypy (y la mayoría de herramientas de pre-commit) buscan su configuración en pyproject.toml desde el directorio raíz del repositorio; mantén toda la configuración ahí y el hook la encontrará sin argumentos adicionales.
Error: Asumir que pre-commit hooks son suficientes y omitir los mismos checks en CI, dejando una vía de escape para quien haga git commit --no-verify.
# ❌ Wrong — CI solo corre tests, no linting ni tipos # .github/workflows/ci.yml - run: pytest # ✅ Right — CI reproduce el mismo gate que los hooks locales - run: pip install pre-commit - run: pre-commit run --all-files - run: pytest
--no-verify es legítimo en situaciones de emergencia (un hotfix a las 3am), pero CI debe ser el backstop que garantiza que nada incorrecto llega a main aunque alguien lo saltee localmente.
N° 205