Durante años, mantener el stack de linting en un proyecto Python serio significaba orquestar varios procesos: flake8 con media docena de plugins, isort corriendo por separado, pyupgrade en un pre-commit hook, y bandit en la pipeline de CI. Cada uno con su propio archivo de configuración, su propia velocidad y sus propios conflictos de versiones. Ruff colapsa todo eso en un único binario escrito en Rust.
La velocidad no es marketing: en un repositorio de ~50 000 líneas, el stack clásico puede tardar 30–60 segundos. Ruff hace lo mismo en menos de un segundo, porque el análisis se ejecuta en paralelo sobre todos los archivos sin levantar un intérprete Python para cada herramienta.
Lo importante no es solo la velocidad, sino el modelo mental: Ruff organiza sus reglas en categorías con prefijo, donde cada prefijo mapea directamente a la herramienta original que implementaba esas reglas. E y W son el territorio de pycodestyle (estilo PEP 8), F viene de pyflakes (imports sin usar, nombres indefinidos), I es isort, B es flake8-bugbear (patrones que no son errores de sintaxis pero sí problemas reales), S es bandit (seguridad), y UP es pyupgrade (modernizar sintaxis a la versión mínima que declaras). Si sabes cuál herramienta usabas antes, ya sabes qué prefijo buscar.
Cuando no habilitas reglas explícitamente, Ruff aplica un subconjunto conservador de E y F: suficiente para el 80% del valor, sin ruido. El punto de inflexión es --select ALL —habilitar absolutamente todo para ver qué te molesta— y a partir de ahí ir a pyproject.toml a declarar qué ignorar intencionalmente. Ese flujo es mucho más honesto que empezar con cero reglas y añadir solo las cómodas.
# pyproject.toml (la configuración vive aquí, no en setup.cfg ni en .flake8)
# No hay imports Python aquí; es TOML puro.
# src/payments/processor.py
import os
import sys
import json
from datetime import datetime
from typing import Optional
import requests # noqa: F401 ← supresión explícita por línea cuando es necesario
SECRET_KEY = "hardcoded-secret-abc123" # S105: hardcoded password — ruff lo detecta
def process_payment(amount, currency="USD", user_id=None):
data = {
"amount": amount,
"currency": currency,
"ts": datetime.utcnow().isoformat(), # UP017: usar datetime.now(tz=...) en Python 3.11+
}
if user_id != None: # E711: comparar con None usando == o != en vez de `is`
data["user_id"] = user_id
unused_var = "esto no se usa nunca" # F841: local variable assigned but never used
try:
response = requests.post("https://api.example.com/pay", json=data)
response.raise_for_status()
return response.json()
except Exception as e: # BLE001 (Bugbear-adjacent): blind except Exception
print(f"Error: {e}")
return None
def validate_amount(amount):
if type(amount) == float: # E721: usa isinstance() en vez de comparar tipos directamente
return amount > 0
return False
# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py311" # UP usa esto para saber qué sintaxis moderna es segura
[tool.ruff.lint]
select = [
"E", # pycodestyle: estilo PEP 8
"F", # pyflakes: errores lógicos básicos
"I", # isort: orden de imports
"B", # flake8-bugbear: patrones problemáticos
"S", # bandit: seguridad
"UP", # pyupgrade: modernización de sintaxis
"BLE", # blind exception catching
"E721", # comparaciones de tipo con ==
]
ignore = [
"S101", # uso de assert — aceptable en tests
"B008", # default mutable argument con llamadas a función — a veces intencional
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S", "BLE001"] # en tests, las reglas de seguridad son ruido
El archivo processor.py está sembrado con exactamente los problemas que Ruff detecta según la categoría. Cuando ejecutas ruff check src/, el output te da el archivo, la línea, el código de regla y una descripción accionable. Cuando ejecutas ruff check --fix src/, Ruff aplica automáticamente las correcciones que son seguras sin ambigüedad: reordena imports (I), elimina variables no usadas en ciertos contextos, actualiza != None a is not None, reemplaza datetime.utcnow() por la forma con timezone. Las correcciones que requieren juicio humano (como qué hacer con SECRET_KEY) solo se reportan.
El target-version = "py311" en la configuración es la clave para que las reglas UP funcionen correctamente. Si declaras py311, Ruff sabe que puede sugerirte datetime.now(UTC) en lugar de datetime.utcnow(), porque esa API existe desde 3.11. Si pones py38, esa sugerencia desaparece porque no sería compatible con tu baseline.
La sección per-file-ignores resuelve el problema más común al activar S (bandit): los tests legítimamente usan assert, hardcodean strings de prueba, y capturan excepciones ampliamente. Silenciar esas categorías solo en tests/ te da seguridad real en el código de producción sin deshabilitar las reglas globalmente.
Errores que debes conocer
Error: Ejecutar ruff check --fix sin control de versiones y perder cambios que parecían automáticos pero alteraron la semántica.
# ❌ Situación: ruff "arregla" un import que en realidad era un re-export intencional # En __init__.py: from .models import User # F401: imported but unused — ruff lo elimina con --fix # ✅ Solución: marca el re-export explícitamente from .models import User as User # el alias `as User` señala que es re-export intencional # O usa __all__ = ["User"]
El truco import X as X es el patrón reconocido por Ruff (y por mypy) para indicar que el import es un re-export público, no un import olvidado.
Error: Activar select = ["ALL"] en producción sin pasar por la fase de revisión, provocando que la pipeline falle por cientos de violaciones de reglas estéticas menores.
# ❌ En pyproject.toml directo a producción [tool.ruff.lint] select = ["ALL"] # ✅ Flujo correcto: primero audita, luego fija el baseline # 1. ruff check --select ALL src/ > violations.txt (solo mira, no bloquea) # 2. Revisa violations.txt, decide qué categorías son ruido para tu proyecto # 3. Añade solo las categorías con valor real a select = [...]
--select ALL como comando de exploración en local y un select curado en pyproject.toml es la separación que te evita un lunes de PRs rotos.
N° 202