El estilo de código es uno de los debates más improductivos que puede tener un equipo. ¿Comillas simples o dobles? ¿Cuánto espacio entre funciones? ¿Hasta dónde llega la línea? Cada uno tiene su opinión, y ninguna de ellas importa si tienes una herramienta que decide por todos.
PEP 8 es la guía de estilo oficial de Python. Define convenciones: líneas de máximo 79 caracteres (o 88 si el equipo lo prefiere), snake_case para funciones y variables (calculate_total, user_name), CamelCase para clases (PaymentProcessor, UserProfile), imports agrupados en tres bloques ordenados (stdlib → third-party → local), y un montón de reglas tipográficas sobre espacios alrededor de operadores. Es la base. El problema es que “guía” significa que puedes ignorarla, y la gente lo hace constantemente, aunque sea por descuido.
black resolvió esto con una postura radical: el formateador es opinionado y no tiene opciones. No le preguntas cómo quieres que formatee; él formatea, y ya está. Lo que produce black es el estándar del proyecto, porque es el único resultado posible. Esto elimina el debate de raíz: no hay nada que discutir porque no hay nada que configurar.
ruff format es el sucesor práctico. Está escrito en Rust, es entre 10 y 100 veces más rápido que black, y produce output compatible. Si tu equipo empieza hoy, usa ruff directamente; si tienes un proyecto con black establecido, la migración es transparente.
El punto de usar estas herramientas no es escribir código “bonito”. Es que cuando el formateador corre automáticamente antes de cada commit, el CI nunca falla por estilo, los diffs en PRs solo contienen cambios lógicos, y los code reviews se concentran en lo que importa.
# pyproject.toml [tool.ruff] line-length = 88 target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I"] # pycodestyle errors, pyflakes, isort ignore = [] [tool.ruff.format] quote-style = "double" indent-style = "space" # Si el proyecto todavía usa black directamente: [tool.black] line-length = 88 target-version = ["py312"]
# src/payments/processor.py
from __future__ import annotations
import decimal
from dataclasses import dataclass, field
from datetime import datetime
import httpx # third-party
from payments.models import Transaction # local
@dataclass
class PaymentProcessor:
"""Procesa pagos contra una API externa."""
base_url: str
timeout: float = 10.0
_history: list[Transaction] = field(default_factory=list, repr=False)
def charge(
self,
amount: decimal.Decimal,
currency: str,
description: str = "",
) -> Transaction:
if amount <= decimal.Decimal("0"):
raise ValueError(f"amount must be positive, got {amount}")
payload = {
"amount": str(amount),
"currency": currency.upper(),
"description": description,
"timestamp": datetime.utcnow().isoformat(),
}
# httpx.post es síncrono aquí a propósito; la versión async está en otro módulo
response = httpx.post(
f"{self.base_url}/charges",
json=payload,
timeout=self.timeout,
)
response.raise_for_status()
transaction = Transaction(**response.json())
self._history.append(transaction)
return transaction
def recent_transactions(self, limit: int = 10) -> list[Transaction]:
return self._history[-limit:]
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff # lint + autofix
args: [--fix]
- id: ruff-format # formateo, equivalente a black
Lo que acabas de ver
El pyproject.toml centraliza la configuración de ambas herramientas. line-length = 88 aparece en los dos bloques porque black popularizó ese valor como compromiso entre el límite histórico de 79 de PEP 8 y la realidad de los monitores modernos. Si cambias ese número en un sitio, cámbialo en el otro, o mejor aún, migra completamente a ruff y elimina la sección [tool.black].
En [tool.ruff.lint], el selector "I" activa las reglas de isort. Esto es importante: ruff no solo formatea, también ordena y agrupa imports automáticamente según PEP 8. El bloque de imports en processor.py (stdlib → httpx → payments.models) existe porque ruff lo habría reorganizado así de todas formas; escribirlo ya correcto es simplemente no generar ruido en el primer commit.
La firma de charge está dividida en múltiples líneas. Eso no es una elección estética —es lo que ruff format (y black) producen cuando una firma supera los 88 caracteres. No lo peleas; lo aceptas, y con el tiempo dejas de notarlo.
El hook de pre-commit tiene dos entradas distintas: ruff para lint y ruff-format para formateo. Son pipelines separados. ruff con --fix corrige automáticamente problemas de estilo y algunos errores de lint antes de que lleguen al staging area. Si algo no puede autocorregirse, el hook falla y te muestra exactamente qué línea tiene el problema. El commit no sucede hasta que el código pase.
Errores que debes conocer
Error: Tener line-length diferente en [tool.ruff.lint] y [tool.ruff.format], lo que hace que el linter reporte líneas largas que el formateador nunca rompe.
# ❌ Wrong [tool.ruff.lint] line-length = 79 [tool.ruff.format] line-length = 88
# ✅ Right [tool.ruff] line-length = 88 # valor compartido por lint y format
El line-length en [tool.ruff] aplica a ambos subsistemas; definirlo una sola vez elimina la inconsistencia.
Error: Agregar ruff-format al pre-commit pero olvidar que modifica archivos en lugar de solo reportar, lo que provoca que el hook falle en el primer run aunque el código quede correcto tras la corrección.
# ❌ Wrong: el desarrollador ve un fallo y no entiende que el archivo ya está corregido - id: ruff-format
# ✅ Right: documentado explícitamente para el equipo - id: ruff-format # Este hook modifica archivos. Si falla, haz git add y vuelve a hacer commit.
No es un bug de la herramienta, es el comportamiento esperado de un formateador: modifica, reporta el cambio como fallo del hook, y tú haces git add del archivo ya formateado antes del siguiente intento.
Error: Ejecutar black y ruff format sobre el mismo archivo en el mismo pipeline, creyendo que son complementarios.
# ❌ Wrong
- repo: https://github.com/psf/black
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
hooks:
- id: ruff-format
# ✅ Right: uno o el otro, nunca los dos
- repo: https://github.com/astral-sh/ruff-pre-commit
hooks:
- id: ruff-format
Aunque el output es compatible, correr ambos duplica trabajo, puede introducir ciclos si hay alguna divergencia menor entre versiones, y confunde a cualquiera que lea la configuración de CI.
Con ruff format en pre-commit y pyproject.toml como fuente de verdad, el estilo deja de ser una conversación y pasa a ser una propiedad del repositorio que se cumple mecánicamente.
N° 201