El Modelo de Pipeline de Análisis Estático
Ruff, Black, mypy y pre-commit se agrupan como "herramientas de calidad de código", pero no son variaciones de un único mecanismo: son tres técnicas genuinamente diferentes para leer código fuente sin ejecutarlo, conectadas en un pipeline. Configuración de Ruff y las páginas que le siguen muestran cómo configurar cada herramienta; esta página explica por qué el linting, el formateo y la comprobación de tipos existen como preocupaciones separadas, y por qué ejecutar las mismas comprobaciones en un editor, un hook de pre-commit y CI es una estrategia deliberada en lugar de una repetición desperdiciada.
Esa distinción importa en el momento en que algo sale mal: un formateador peleando con un linter por la longitud de línea, un comprobador de tipos señalando algo que Ruff ignoró felizmente, o un hook de pre-commit que pasa localmente pero falla en CI, todo se remonta a no saber cuál de estos tres mecanismos es realmente responsable de la comprobación en cuestión.
Resumen
- Las herramientas de análisis estático responden a tres preguntas distintas sobre el código fuente sin ejecutarlo: ¿es sintáctica/estilísticamente correcto (linting)?, ¿está formateado consistentemente (formateo)? y ¿pasa la comprobación de tipos (análisis semántico)? - y cada una utiliza un mecanismo interno diferente para hacerlo.
- Por Qué Importa: Tratar el linting, el formateo y la comprobación de tipos como una única "puerta de calidad" indiferenciada conduce a pipelines mal configurados: reglas que luchan entre sí, comprobaciones en la etapa incorrecta del pipeline o errores de tipo confundidos con problemas de estilo.
- Conceptos Clave: árbol de sintaxis abstracta (AST), regla de linting, idempotencia del formateador, inferencia de tipos, shift-left, hook de pre-commit.
- Cuándo Usar Este Modelo: Decidir dónde pertenece una nueva comprobación (editor, pre-commit o solo CI), diagnosticar por qué un formateador y un linter no están de acuerdo, explicar a un equipo por qué la comprobación de tipos no es "simplemente un linter más estricto", y diseñar un pipeline que falle rápido sin volverse lento o ruidoso.
- Limitaciones / Compensaciones: Ninguna de estas herramientas demuestra que el programa es correcto: un linter detecta patrones conocidos incorrectos, un formateador impone consistencia y un comprobador de tipos verifica la consistencia interna de los tipos declarados, pero un programa completamente lintado, formateado y con tipos comprobados aún puede tener una lógica de negocio incorrecta.
- Temas Relacionados: el pipeline CI/CD en el que se ejecutan estas puertas, pytest como la capa que realmente verifica el comportamiento, herramientas de editor y servidores de lenguaje.
Fundamentos
El linting y el formateo comienzan ambos con el mismo primer paso: analizar el código fuente en un árbol de sintaxis abstracta (AST), una representación estructurada de la gramática del código sin comentarios ni espacios en blanco adjuntos, pero luego hacen cosas completamente diferentes con él.
Un linter recorre ese árbol buscando patrones que coincidan con problemas conocidos: una importación no utilizada, un except: genérico, un argumento por defecto mutable: cada patrón es una regla, y el disparo de una regla produce un diagnóstico con una ubicación y un mensaje, opcionalmente junto con una corrección automática.
Un formateador, por el contrario, no busca problemas en absoluto; toma el mismo AST (o un árbol de sintaxis concreto similar) y lo reimprime según un estilo fijo, descartando por completo los espacios en blanco y saltos de línea originales y regenerándolos desde cero.
Es por eso que ejecutar un formateador dos veces en código ya formateado no produce cambios: la idempotencia es el contrato principal del formateador, y cualquier formateador que no pueda garantizarlo está roto por definición. Un linter no tiene un contrato equivalente, porque arreglar una violación a veces puede revelar o introducir otra (eliminar una importación no utilizada podría revelar una variable no utilizada que dependía de ella), por lo que el linting y la corrección se ejecutan típicamente hasta un punto fijo o con un número limitado de pasadas en lugar de asumir que convergen en una sola.
Una analogía útil: un formateador es un editor de copias que impone un estilo de casa sin juicios: cada manuscrito sale formateado de la misma manera independientemente del contenido. Un linter se acerca más a un corrector de pruebas que señala errores específicos conocidos: un modificador colgado aquí, un tiempo verbal inconsistente allá, cada uno señalado individualmente, con algunas señales fáciles de corregir automáticamente y otras que requieren una decisión humana.
# pyproject.toml - dos mecanismos diferentes, un archivo de configuración
[tool.ruff.lint]
select = ["E", "F", "B"] # comparación de patrones sobre el AST
[tool.ruff.format]
quote-style = "double" # reimpresión determinista, sin comparación de patronesMecánicas e Interacciones
Un comprobador de tipos como mypy o pyright es un tipo de herramienta completamente diferente, y confundirlo con "un linter más estricto" es el malentendido más común en este espacio.
Donde un linter evalúa cada patrón en gran medida en contexto local, un comprobador de tipos realiza inferencia de tipos a través de todo el grafo de llamadas alcanzable: lee cada firma de función y anotación, propaga los tipos inferidos a través de asignaciones y retornos, y verifica que cada operación sea consistente con los tipos que derivó, detectando un None pasado donde se esperaba un str incluso cuando las dos líneas involucradas están en archivos completamente diferentes.
Ese razonamiento de programa completo es también la razón por la que la comprobación de tipos es más lenta que el linting en una base de código comparable, y por la que generalmente se ejecuta como un paso de CI separado con su propia caché en lugar de integrarse en la misma pasada que Ruff.
Los tres mecanismos interactúan a través de la configuración mucho más que a través de lógica compartida, y la mayor parte de la fricción que encuentran los equipos proviene de esa interacción en lugar de de una sola herramienta.
El ejemplo clásico es la longitud de línea: un formateador impone un comportamiento de ajuste de línea de forma determinista, por lo que una regla de linting que también señala líneas largas (E501) a veces diferirá de lo que el formateador acaba de producir, razón por la cual la propia guía de configuración de Ruff deshabilita E501 y deja que el formateador se encargue de la longitud de línea por completo en lugar de ejecutar dos autoridades sobre la misma pregunta.
La ordenación de importaciones muestra el mismo patrón a la inversa: es genuinamente una preocupación de linting (hay un orden correcto y verificable) que las reglas I de Ruff absorbieron de la herramienta independiente isort, difuminando la línea entre "linter" y "formateador" para ese comportamiento específico sin cambiar lo que fundamentalmente hace cada mecanismo.
Shift-left describe dónde en el ciclo de desarrollo se ejecutan realmente estas comprobaciones, y es una estrategia genuina en lugar de una repetición redundante: una integración en el editor (un servidor de lenguaje) muestra un error de lint o tipo mientras escribes, un hook de pre-commit captura cualquier cosa que se haya escapado antes de que llegue al control de versiones, y CI es la puerta final e innegociable que ejecuta las mismas comprobaciones en un entorno limpio que ninguna máquina local puede eludir.
Cada capa existe porque las anteriores son opcionales o pueden ser eludidas (un hook puede omitirse con --no-verify, una extensión del editor puede deshabilitarse): CI es lo que realmente impone el estándar, y las capas anteriores existen puramente para hacer que el fallo de esa puerta sea raro y barato de arreglar.
Consideraciones Avanzadas y Aplicaciones
La ubicación importa más que la elección de la herramienta una vez que un pipeline escala más allá de un puñado de archivos, porque cada mecanismo tiene un perfil de costo diferente que determina dónde pertenece. El formateo y las reglas de linting rápidas son lo suficientemente baratas como para ejecutarse en cada pulsación de tecla en un editor y en cada commit en un hook; el análisis de programa completo de la comprobación de tipos suele ser demasiado lento para un hook de pre-commit en una base de código grande y pertenece a CI con su propia caché incremental, ejecutándose con menos frecuencia pero nunca omitiéndose.
Las violaciones auto-corregibles y las estructurales también exigen caminos de remediación diferentes, y los pipelines que no las distinguen crean una falsa confianza: una regla con una auto-corrección segura (importación no utilizada, estilo de comillas) se puede corregir mecánicamente y volver a verificar en la misma ejecución, mientras que una violación estructural (una discrepancia de tipo real, un error real que las reglas de "bugbear" del linter detectaron) requiere que un humano cambie la lógica real, y ninguna bandera --fix podrá resolverlo.
La adopción gradual de reglas más estrictas -mover una base de código heredada de check_untyped_defs a strict = true completo en mypy, por ejemplo- es realmente una afirmación sobre el ruido tolerable: habilitar todas las reglas a la vez en una base de código sin tipos produce miles de diagnósticos que enseñan a los ingenieros a ignorar la herramienta por completo, mientras que una implementación escalonada mantiene la relación señal/ruido lo suficientemente alta como para que las personas actúen sobre ella.
| Mecanismo | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Linting (Ruff, pylint) | Rápido, detecta patrones incorrectos conocidos, muchos corregibles automáticamente | Solo tan bueno como su conjunto de reglas; omite errores de lógica de programa completo | Cada commit y ejecución de CI, retroalimentación de alta frecuencia |
| Formateo (Ruff format, Black) | Cero debate de estilo, siempre idempotente | No tiene opinión sobre la corrección, solo la apariencia | Cada commit; nunca debería ser una discusión de revisión manual |
| Comprobación de tipos (mypy, pyright) | Detecta errores de lógica reales entre archivos (seguridad contra None, formas incorrectas) | Lento, análisis de programa completo; ruidoso en código heredado sin tipos | Puerta de CI, ejecutado por separado con caché incremental |
Conceptos Erróneos Comunes
- "Un comprobador de tipos es solo un linter con más reglas." Realiza inferencia de tipos de programa completo a través de tu grafo de llamadas, no comparación de patrones local: es un tipo de análisis fundamentalmente diferente (y más lento), no una extensión del mismo.
- "Si el formateador y el linter no están de acuerdo, uno de ellos está roto." Responden a preguntas diferentes por diseño; el desacuerdo sobre algo como la longitud de línea generalmente significa que la misma preocupación está configurada en ambos lugares y debe ser propiedad de uno.
- "Ejecutar la misma regla de linting en el editor, pre-commit y CI es redundante." Cada capa existe porque las anteriores se pueden omitir: CI es la única capa que realmente impone algo; las capas anteriores existen para hacer que el fallo de CI sea raro.
- "100% libre de linting y tipos significa que el código es correcto." Ambas herramientas verifican la consistencia interna contra patrones conocidos o tipos declarados; ninguna de ellas verifica que la lógica de negocio haga lo correcto, que es para lo que sirven las pruebas.
- "Habilitar el modo estricto en todas partes de inmediato es la opción responsable." En una base de código heredada sin tipos o sin linting, produce tantos diagnósticos que la herramienta es ignorada; la estrictez gradual y escalonada mantiene la señal utilizable.
Preguntas Frecuentes
¿Cuál es la diferencia real entre lo que hacen un linter y un formateador?
Un linter busca patrones conocidos como incorrectos en un AST e informa (o corrige automáticamente) violaciones; un formateador reimprime el mismo AST con un estilo determinista, sin noción de "problemas" en absoluto.
¿Por qué ejecutar un formateador dos veces en el mismo archivo no produce cambios?
La idempotencia es una propiedad definitoria del formateador: dado que siempre regenera los espacios en blanco y los saltos de línea a partir de las mismas reglas fijas, formatear código ya formateado es una operación nula por construcción.
¿La ordenación de importaciones es una preocupación de linting o de formateo?
Es genuinamente una preocupación de linting (hay un orden correcto y verificable), razón por la cual las reglas I de Ruff absorbieron el comportamiento de la herramienta independiente isort en lugar de integrarlo en el formateador.
¿Por qué un comprobador de tipos es más lento que un linter en la misma base de código?
Un linter evalúa principalmente patrones en contexto local; un comprobador de tipos realiza inferencia en todo el grafo de llamadas alcanzable, propagando tipos a través de cada asignación y retorno: ese razonamiento de programa completo es inherentemente más costoso.
¿Por qué deshabilitar `E501` (línea demasiado larga) junto con un formateador tiene sentido?
Porque el formateador ya se encarga del ajuste de línea de forma determinista; una regla de linting separada para la misma preocupación puede diferir de lo que el formateador acaba de producir, por lo que la mayoría de las configuraciones de Ruff dejan que una autoridad se encargue de la longitud de línea.
¿Por qué ejecutar las mismas reglas de linting en un editor, un hook de pre-commit y CI en lugar de solo en CI?
Cada capa anterior es un ciclo de retroalimentación más barato y rápido que también se puede omitir: el editor detecta problemas mientras escribes, el hook detecta cualquier cosa antes de que se confirme, y CI es la única capa que nadie puede omitir, por lo que juntas hacen que el fallo de CI sea raro en lugar de duplicar el esfuerzo.
¿Puede una regla de linting y un comprobador de tipos señalar el mismo error?
Ocasionalmente, pero generalmente no: las reglas de estilo "bugbear" de un linter detectan patrones peligrosos conocidos localmente (como un argumento por defecto mutable), mientras que un comprobador de tipos detecta inconsistencias entre archivos que una regla de comparación de patrones no puede ver.
¿Por qué adoptar `strict = true` en mypy de golpe suele salir mal en una base de código existente?
Expone todas las brechas de tipado no verificadas previamente simultáneamente, a menudo miles de diagnósticos en una base de código grande sin tipos, lo que enseña a los ingenieros a ignorar la herramienta en lugar de corregir los problemas gradualmente.
¿Una violación de linting auto-corregible significa que el problema subyacente es trivial?
No siempre: una auto-corrección (como eliminar una importación no utilizada) es segura porque es mecánica, pero una violación estructural que el mismo linter señala (un error de lógica real detectado por "bugbear") todavía requiere que un humano cambie el código real.
¿Está garantizado que una base de código completamente lintada, formateada y con tipos correctos sea correcta?
No: las tres herramientas verifican la consistencia interna (estilo, formato, tipos declarados), no que la lógica de negocio haga lo correcto; eso es específicamente para lo que sirve un conjunto de pruebas.
¿Por qué los hooks de pre-commit a veces pasan localmente pero fallan en CI?
Generalmente una discrepancia de versión o caché entre el hook instalado localmente y la versión fijada que CI ejecuta, o un hook que se omitió localmente (--no-verify) - CI ejecutándose en un entorno limpio es exactamente lo que lo convierte en la puerta de enlace autorizada.
¿La comprobación de tipos debe ejecutarse en archivos de prueba de la misma manera que en el código de la aplicación?
Muchos equipos relajan las reglas de comprobación de tipos para tests/ (a través de una anulación de mypy) ya que el código de prueba tiene diferentes necesidades ergonómicas que el código de producción, mientras mantienen el código de la aplicación bajo estrictez completa.
Relacionados
- Configuración de Ruff - configuración directa del mecanismo de lint y formato basado en AST
- Black / Ruff Format - el mecanismo de reimpresión determinista en profundidad
- Reglas de Lint Esenciales - qué categorías de reglas aportan señal real
- mypy y pyright en CI - el mecanismo de inferencia de tipos de programa completo
- Hooks de pre-commit - la capa de "shift-left" antes del control de versiones
- Linting en CI/CD - la capa de imposición no evitable
Versiones de la Pila: Esta página fue escrita para Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.