de esos idiomas que pueden hacerte sentir productivo casi de inmediato.
Esa es una gran parte de por qué es tan popular. Pasar de una idea a un código funcional puede ser muy rápido. No necesitas muchos andamios sólo para probar una idea. Un poco de análisis de entrada, tal vez algunas funciones, unirlas y muy a menudo tendrás algo útil frente a ti en cuestión de minutos.
La desventaja es que Python también puede ser muy indulgente en lugares donde a veces preferirías que no lo fuera.
Con bastante gusto asumirá que existe una clave de diccionario cuando no es así. Le permitirá pasar estructuras de datos con formas ligeramente diferentes hasta que una finalmente se rompa en tiempo de ejecución. Permitirá que un error tipográfico sobreviva más de lo que debería. Y tal vez, disimuladamente, permitirá que el código sea "correcto" sin dejar de ser demasiado lento para su uso en el mundo real.
Es por eso que me he interesado más en los flujos de trabajo de desarrollo de código en general que en una única técnica de prueba.
Cuando la gente habla de calidad del código, la conversación suele ir directamente a las pruebas. Las pruebas son importantes y las uso constantemente, pero no creo que deban soportar toda la carga. Sería mejor si la mayoría de los errores se detectaran incluso antes de ejecutar el código. Quizás algunos problemas deberían solucionarse tan pronto como guarde su archivo de código. Otros, cuando confirmas tus cambios en GitHub. Y si todo pasa bien, tal vez desee ejecutar una serie de pruebas para verificar que el código se comporta correctamente y funciona lo suficientemente bien como para resistir el contacto en el mundo real.
En este artículo, quiero analizar un conjunto de herramientas que puede utilizar para crear un flujo de trabajo de Python para automatizar las tareas mencionadas anteriormente. No es una configuración empresarial gigante ni una plataforma DevOps elaborada. Solo una cadena de herramientas práctica y relativamente simple que ayuda a detectar errores en su código antes de implementarlo en producción.
Para concretar esto, voy a utilizar un ejemplo pequeño pero realista. Imagine que estoy creando un módulo de Python que procesa cargas útiles de pedidos, calcula totales y genera resúmenes de pedidos recientes. He aquí un primer paso deliberadamente duro.
desde fecha y hora importar fecha y hora importar json def normalize_order(order): creado = datetime.fromisoformat(order["created_at"]) return { "id": pedido["id"], "customer_email": order.get("customer_email"), "items": pedido["items"], "created_at": creado, "discount_code": order.get("discount_code"), } def calcular_total(pedido): total = 0 descuento = Ninguno para el artículo en pedido["artículos"]: total += artículo["precio"] * artículo["cantidad"] if order.get("código_descuento"): descuento = 0,1 total *= 0,9 ronda de devolución (total, 2) def build_order_summary(orden): normalizado = normalize_order(orden); total = calcular_total(pedido) return { "id": normalizado["id"], "email": normalizado["customer_email"].lower(), "created_at": normalizado["created_at"].isoformat(), "total": total, "item_count": len(normalizado["items"]), } def Recent_order_totals(pedidos): resúmenes =[]para ordenar en pedidos: summaries.append(build_order_summary(order)) summaries.sort(key=lambda x: x["created_at"], revertir=True) devolver resúmenes[:10]
Hay muchas cosas que me gustan de un código como este cuando "te mueves rápido y rompes cosas". Es breve y legible, y probablemente incluso funcione en las primeras entradas de muestra que pruebe.
Pero también hay varios errores o problemas de diseño esperando entre bastidores. Si falta el correo electrónico del cliente, por ejemplo, el método .lower() generará un AttributeError. También se supone que la variable items siempre contiene las claves esperadas. Hay una importación no utilizada y una variable sobrante de lo que parece ser una refactorización incompleta. Y en la función final, se ordena todo el conjunto de resultados aunque solo se necesitan los 10 elementos más recientes. Ese último punto es importante porque queremos que nuestro código sea lo más eficiente posible. Si solo necesitamos los diez primeros, debemos evitar ordenar completamente el conjunto de datos siempre que sea posible.
Es en un código como este donde un buen flujo de trabajo comienza a amortizarse.
Dicho esto, echemos un vistazo a algunas de las herramientas que puede utilizar en su proceso de desarrollo de código, lo que garantizará que su código tenga las mejores posibilidades posibles de ser correcto, mantenible y eficaz. Todas las herramientas que discutiré se pueden descargar, instalar y usar de forma gratuita.
Tenga en cuenta que algunas de las herramientas que menciono son multipropósito. Por ejemplo, parte del formateo que puede realizar la utilidad negra también se puede realizar con la herramienta ruff. A menudo, depende sólo de tus preferencias personales cuáles utilizas.
Herramienta n.º 1: código legible sin ruido de formato
La primera herramienta que suelo instalar se llama Black. Black es un formateador de código Python. Su trabajo es muy simple, toma su código fuente y aplica automáticamente un estilo y formato consistente.
Instalación y uso
Instálelo usando pip o su administrador de paquetes Python preferido. Después de eso, puedes ejecutarlo así,
$ negro tu_python_file.py o $ python -m negro tu_python_file
Black requiere la versión 3.10 o posterior de Python para ejecutarse.
Usar un formateador de código puede parecer cosmético, pero creo que los formateadores son más importantes de lo que la gente admite a veces. No querrás gastar energía mental decidiendo cómo debe terminar una llamada a una función, dónde debe ir un salto de línea o si has formateado un diccionario "bastante bien". Su código debe ser coherente para que pueda centrarse en la lógica en lugar de en la presentación.
Supongamos que ha escrito esta función rápidamente.
def build_order_summary(orden): normalizado=normalize_order(orden); total=calcular_total(pedido) return {"id":normalizado["id"],"email":normalizado["customer_email"].lower(),"created_at":normalized["created_at"].isoformat(),"total":total,"item_count":len(normalizado["items"])}
Es complicado, pero Black lo convierte en esto.
def build_order_summary(order): normalizado = normalize_order(order) total = calcular_total(order) return { "id": normalizado["id"], "email": normalizado["customer_email"].lower(), "created_at": normalizado["created_at"].isoformat(), "total": total, "item_count": len(normalizado["items"]), }
Black no ha solucionado ninguna lógica empresarial aquí. Pero ha hecho algo extremadamente útil: ha hecho que el código sea más fácil de inspeccionar. Cuando el formato desaparece como fuente de fricción, cualquier problema real de codificación se vuelve mucho más fácil de ver.
Black se puede configurar de muchas maneras diferentes, sobre las cuales puedes leer en su documentación oficial. (Los enlaces a esta y a todas las herramientas mencionadas se encuentran al final del artículo)
Herramienta #2: Detectar los pequeños errores sospechosos
Una vez que se maneja el formato, normalmente agrego Ruff al proceso. Ruff es un linter de Python escrito en Rust. Ruff es rápido, eficiente y muy bueno en lo que hace.
Instalación y uso
Al igual que Black, Ruff se puede instalar con cualquier administrador de paquetes de Python.
$ pip install ruff $ # Y usado así $ ruff revisa your_python_code.py
Linting es útil porque muchos errores comienzan como pequeños detalles sospechosos. No fallas lógicas profundas ni casos extremos inteligentes. Código ligeramente incorrecto.
Por ejemplo, digamos que tenemos el siguiente código simple. En nuestro módulo de muestra, por ejemplo, hay un par de importaciones no utilizadas y una variable asignada pero que nunca se necesita:
desde fecha y hora importar fecha y hora importar json def calcular_total(pedido): total = 0 descuento = 0 para el artículo en pedido["artículos"]: total += artículo["precio"] * artículo["cantidad"] if pedido.get("código_descuento"): total *= 0,9 ronda de devolución (total, 2)
Ruff puede atraparlos inmediatamente:
$ ruff check test1.py F401 [*] `datetime.datetime` importado pero no utilizado –> test1.py:1:22 | 1 | desde fecha y hora importar fecha y hora | ^^^^^^^^ 2 | importar json | ayuda: Eliminar importación no utilizada: `datetime.datetime` F401 [*] `json` importado pero no utilizado –> test1.py:2:8 | 1 | desde fecha y hora importar fecha y hora 2 | importar json | ^^^^ 3 | 4 | def calcular_total(orden): | ayuda: Eliminar importación no utilizada: `json` F841 La variable local `discount` está asignada pero nunca se usa –> test1.py:6:5 | 4 | def calcular_total(orden): 5 | total = 0 6 | descuento = 0 | ^^^^^^^^ 7 | 8 | para el artículo en orden["artículos"]: | ayuda: Eliminar la asignación a la variable no utilizada `descuento` Se encontraron 3 errores. [*] 2 reparables con la opción `–fix` (1 solución oculta se puede habilitar con la opción `–unsafe-fixes`).
Herramienta #3: Python comienza a sentirse mucho más seguro
El formateo y el linting ayudan, pero ninguno de ellos aborda realmente la fuente de gran parte del problema en Python: las suposiciones sobre los datos.
Ahí es donde entra mypy. Mypy es un verificador de tipos estáticos para Python.
Instalación y uso
Instálelo con pip, luego ejecútelo así
$ pip install mypy $ # Para ejecutar use este $ mypy test3.py
Mypy ejecutará una verificación de tipo en su código (sin ejecutarlo realmente). Este es un paso importante porque muchos errores de Python son en realidad errores de forma de datos. Asumes que existe un campo. Asumes que un valor es una cadena o que una función devuelve una cosa cuando en realidad a veces devuelve otra.
Para verlo en acción, agreguemos algunos tipos a nuestro ejemplo de pedido.
desde fecha y hora importar fecha y hora desde escribir importar NotRequired, clase TypedDict Artículo(TypedDict): precio: cantidad flotante: clase int RawOrder(TypedDict): id: str elementos: lista[Artículo] creado_en: str correo electrónico del cliente: NotRequired[str] código_descuento: NotRequired[str] clase Orden Normalizado(TypedDict): id: str correo electrónico del cliente: str | Ninguno artículos: lista[Artículo] creado_en: fecha y hora código_descuento: cadena | Ninguno clase OrderSummary(TypedDict): id: str correo electrónico: str creado_at: str total: flotante item_count: int
Ahora podemos anotar nuestras funciones.
def normalize_order(pedido: RawOrder) -> NormalizedOrder: return { "id": pedido["id"], "customer_email": order.get("customer_email"), "items": pedido["items"], "created_at": datetime.fromisoformat(order["created_at"]), "discount_code": order.get("discount_code"), } def calcular_total(order: RawOrder) -> float: total = 0,0 para el artículo en pedido["artículos"]: total += artículo["precio"] * artículo["cantidad"] if order.get("discount_code"): total *= 0,9 return round(total, 2) def build_order_summary(order: RawOrder) -> OrderSummary: normalizado = normalize_order(pedido) total = calcular_total(pedido) return { "id": normalizado["id"], "email": normalizado["customer_email"].lower(), "created_at": normalizado["created_at"].isoformat(), "total": total, "item_count": len(normalizado["items"]), }
Ahora el error es mucho más difícil de ocultar. Por ejemplo,
$ mypy test3.py test.py:36: error: el elemento "Ninguno" de "str | Ninguno" no tiene el atributo "inferior" [union-attr] Se encontró 1 error en 1 archivo (verificó 1 archivo fuente)
customer_email proviene de order.get(“customer_email”), lo que significa que puede faltar y, por lo tanto, se evalúa como Ninguno. Mypy rastrea ese asstr | Ninguno, y rechaza correctamente llamar a .lower() sin manejar primero el caso Ninguno.
Puede parecer algo sencillo, pero creo que es una gran victoria. Mypy te obliga a ser más honesto acerca de la forma de los datos que realmente estás manejando. Convierte vagas sorpresas durante el tiempo de ejecución en comentarios tempranos y más claros.
Herramienta #4: Probar, probar 1..2..3
Al comienzo de este artículo, identificamos tres problemas en nuestro código de procesamiento de pedidos: un bloqueo cuando falta el correo electrónico del cliente, suposiciones no comprobadas sobre las claves de los artículos y una clasificación ineficiente, a la que volveremos más adelante. Black, Ruff y Mypy ya nos han ayudado a abordar estructuralmente los dos primeros. Pero las herramientas que analizan código estáticamente sólo pueden llegar hasta cierto punto. En algún momento, deberás verificar que el código realmente se comporte correctamente cuando se ejecuta. Para eso está pytest.
Instalación y uso
$ pip instala pytest $ $ # ejecútalo con $ pytest your_test_file.py
Pytest tiene una gran cantidad de funcionalidades, pero su característica más simple y útil es también la más directa: la directiva de afirmación. Si la condición que usted afirma es falsa, la prueba falla. Eso es todo. No hay un marco elaborado que aprender antes de poder escribir algo útil.
Suponiendo que ahora tenemos una versión del código que maneja correctamente los correos electrónicos faltantes, junto con un ejemplo base_order, aquí hay una prueba que protege la lógica de descuento:
import pytest @pytest.fixture def base_order(): return { "id": "order-123", "customer_email": "[email protected]", "created_at": "2025-01-15T10:30:00", "items": [ {"price": 20, "quantity": 2}, {"price": 5, "quantity": 1}, ], } def test_calculate_total_applies_10_percent_discount(base_order): base_order["discount_code"] = "SAVE10" total = calcular_total(base_order) subtotal = (20 * 2) + (5 * 1) esperado = subtotal * 0.9 afirmar total == esperado
Y aquí están las pruebas que protegen el manejo del correo electrónico, específicamente el bloqueo que señalamos al principio, donde llamar a .lower() en un correo electrónico faltante provocaría que toda la función se desactivara:
def test_build_order_summary_returns_valid_email(base_order): resumen = build_order_summary(base_order) afirmar "correo electrónico" en resumen afirmar resumen["correo electrónico"].endswith("@example.com") def test_build_order_summary_when_email_missing(base_order): base_order.pop("customer_email") resumen = build_order_summary(base_order) afirmar resumen["correo electrónico"] == ""
Esa segunda prueba también es importante. Sin él, un correo electrónico perdido es una suposición silenciosa: código que funciona bien en desarrollo y luego arroja un AttributeError la primera vez que llega un pedido real sin ese campo. Con él, la suposición es explícita y se verifica cada vez que se ejecuta el conjunto de pruebas.
Esta es la división del trabajo que vale la pena tener en cuenta. Ruff detecta importaciones no utilizadas y variables muertas. Mypy detecta malas suposiciones sobre los tipos de datos. Pytest detecta algo diferente: protege el comportamiento. Cuando cambias la forma en que build_order_summary maneja los campos faltantes, o refactorcalcula_total, pytest es lo que te dice si has roto algo que funcionaba anteriormente. Se trata de un tipo diferente de red de seguridad y opera a un nivel diferente de todo lo anterior.
Herramienta #5: Porque tu memoria no es un sistema confiable de control de calidad
Incluso con una buena cadena de herramientas, todavía hay una debilidad obvia: puedes olvidarte de ejecutarla. Ahí es donde una herramienta como el compromiso previo entra en juego. La confirmación previa es un marco para administrar y mantener enlaces en varios idiomas, como los que se ejecutan cuando confirmas código en GitHub o lo envías a tu repositorio.
Instalación y uso
La configuración estándar es instalarlo con pip, luego agregar un archivo .pre-commit-config.yaml y ejecutar la instalación previa a la confirmación para que los enlaces se ejecuten automáticamente antes de cada confirmación en su sistema de control de código fuente, por ejemplo, GitHub.
Una configuración simple podría verse así:
repositorios: – repositorio: https://github.com/psf/black rev: 24.10.0 ganchos: – id: black – repositorio: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.13 ganchos: – id: ruff – id: ruff-format – repositorio: ganchos locales: – id: nombre mypy: entrada mypy: idioma mypy: tipos de sistema: [python] etapas: [pre-push] – id: nombre de pytest: entrada de pytest: idioma de pytest: sistema pass_filenames: false etapas: [pre-push]
Ahora lo ejecutas con,
$ instalación de confirmación previa instalación de confirmación previa instalada en .git/hooks/pre-commit $ instalación de confirmación previa –comprobación previa de tipo gancho instalada en .git/hooks/pre-push
A partir de ese momento, las comprobaciones se ejecutan automáticamente cuando su código se cambia y se confirma/envía.
git commit → activa black, ruff, formato ruff git push → activa mypy y pytest
He aquí un ejemplo.
Digamos que tenemos el siguiente código Python en el archivo test1.py
desde fecha y hora importar fecha y hora importar json def calcular_total(pedido): total = 0 descuento = 0 para el artículo en pedido["artículos"]: total += artículo["precio"] * artículo["cantidad"] if pedido.get("código_descuento"): total *= 0,9 ronda de devolución (total, 2)
Cree un archivo llamado .pre-commit-config.yaml con el código YAML de arriba. Ahora, si git rastrea test1.py, este es el tipo de resultado que se espera cuando lo confirma.
$ git commit test1.py [INFO] Inicializando el entorno para https://github.com/psf/black. [INFO] Inicializando el entorno para https://github.com/astral-sh/ruff-pre-commit. [INFO] Entorno de instalación para https://github.com/psf/black. [INFO] Una vez instalado este entorno será reutilizado. [INFO] Esto puede tardar unos minutos… [INFO] Entorno de instalación para https://github.com/astral-sh/ruff-pre-commit. [INFO] Una vez instalado este entorno será reutilizado. [INFO] Esto puede tardar unos minutos… negro………………………………………………………..Error – ID del gancho: negro – los archivos fueron modificados por este gancho reformateado test1.py ¡Todo listo! ✨ 🍰 ✨ 1 archivo reformateado. ruff (alias heredado)………………………………………….Falló – ID de gancho: ruff – Código de salida: 1 test1.py:1:22: F401 [*] `datetime.datetime` importado pero no utilizado | 1 | desde fecha y hora importar fecha y hora | ^^^^^^^^ F401 2 | importar json | = ayuda: Eliminar importación no utilizada: `datetime.datetime` test1.py:2:8: F401 [*] `json` importado pero no utilizado | 1 | desde fecha y hora importar fecha y hora 2 | importar json | ^^^^ F401 | = ayuda: Eliminar importación no utilizada: `json` test1.py:7:5: F841 La variable local `descuento` está asignada pero nunca se usa | 5 | def calcular_total(orden): 6 | total = 0 7 | descuento = 0 | ^^^^^^^^ F841 8 | 9 | para el artículo en orden["artículos"]: | = ayuda: Eliminar asignación a la variable no utilizada `descuento` Se encontraron 3 errores. [*] 2 reparables con la opción `–fix` (1 solución oculta se puede habilitar con la opción `–unsafe-fixes`).
Herramienta #6: Porque el código “correcto” aún se puede descifrar
Hay una última categoría de problemas que creo que se subestima al desarrollar código: el rendimiento. Una función puede ser lógicamente correcta y aun así ser incorrecta en la práctica si es demasiado lenta o requiere demasiada memoria.
Una herramienta de creación de perfiles que me gusta para esto se llama py-spy. Py-spy es un generador de perfiles de muestreo para programas Python. Puede perfilar Python sin reiniciar el proceso ni modificar el código. Esta herramienta es diferente de las demás que hemos analizado, ya que normalmente no la usaría en un proceso automatizado. En cambio, se trata más bien de un proceso único que se ejecuta con código que ya fue formateado, modificado, verificado y probado.
Instalación y uso
$ pip instalar py-espía
Ahora volvamos al ejemplo de los “diez primeros”. Aquí está la función original nuevamente:
Aquí está la función original nuevamente:
def totales_pedidos_recientes(pedidos): resúmenes =[]para ordenar en pedidos: summaries.append(build_order_summary(order)) summaries.sort(key=lambda x: x["created_at"], revertir=True) devolver resúmenes[:10]
Si todo lo que tengo es una colección sin clasificar en la memoria, entonces sí, todavía necesitas algo de lógica de ordenamiento para saber cuáles son las diez más recientes. El punto no es evitar ordenar por completo, sino evitar hacer una clasificación completa de todo el conjunto de datos si solo necesito los diez mejores. Un generador de perfiles te ayuda a llegar a ese nivel más preciso.
Hay muchos comandos diferentes que puedes ejecutar para perfilar tu código usando py-spy. Quizás la más sencilla sea:
$ py-spy top python test3.py Recopilación de muestras de 'python test3.py' (python v3.11.13) Total de muestras 100 GIL: 22,22 %, activo: 51,11 %, subprocesos: 1 %Own %Total OwnTime Función TotalTime (nombre de archivo) 16,67 % 16,67 % 0,160 s 0,160 s _path_stat () 13,33% 13,33% 0,120s 0,120s get_data () 7,78% 7,78% 0,070s 0,070s _compile_bytecode () 5,56% 6,67% 0,060s 0,070s _init_module_attrs () 2,22% 2,22% 0,020s 0,020s _classify_pyc () 1,11% 1,11% 0,010s 0,010s _check_name_wrapper () 1,11% 51,11% 0,010s 0,490s _load_unlocked () 1,11% 1,11% 0,010s 0,010s cache_from_source () 1,11% 1,11% 0,010s 0,010s _parse_sub (re/_parser.py) 1,11% 1,11% 0,010s 0,010s (importlib/metadata/_collections.py) 0,00% 51,11% 0,010s 0,490 s _find_and_load () 0,00 % 4,44 % 0,000 s 0,040 s (pigmentos/formateadores/__init__.py) 0,00 % 1,11 % 0,000 s 0,010 s _parse (re/_parser.py) 0,00 % 0,00 % 0,000 s 0,010s _path_importer_cache () 0,00% 4,44% 0,000s 0,040s (pygments/formatter.py) 0,00% 1,11% 0,000s 0,010s compilar (re/_compiler.py) 0,00% 50,00% 0,000s 0,470s (_pytest/_code/code.py) 0,00% 27,78% 0,000s 0,250s get_code () 0,00% 1,11% 0,000s 0,010s (importlib/metadata/_adapters.py) 0,00% 1,11% 0,000s 0,010s (email/charset.py) 0.00% 51.11% 0.000s 0.490s (pytest/__init__.py) 0.00% 13.33% 0.000s 0.130s _find_spec () Presione Control-C para salir, o ? por ayuda.
top le brinda una vista en vivo de qué funciones consumen más tiempo, lo que la convierte en la forma más rápida de orientarse antes de hacer algo más detallado.
Una vez que nos demos cuenta de que puede haber un problema, podemos considerar implementaciones alternativas de nuestro código. En nuestro caso de ejemplo, una opción sería usar heapq.nlargest en nuestra función:
desde fecha y hora importar fecha y hora desde heapq importar nlargest def Recent_order_totals(orders): return nlargest( 10, (build_order_summary(order) para orden en pedidos), key=lambda x: datetime.fromisoformat(x["created_at"]),)
El nuevo código aún realiza comparaciones, pero evita ordenar completamente cada resumen para descartarlos casi todos. En mis pruebas con entradas grandes, la versión que usaba heapq fue entre 2 y 3 veces más rápida que la función original. Y en un sistema real, la mejor optimización a menudo es no resolver esto en Python en absoluto. Si los datos provienen de una base de datos, normalmente preferiría solicitarle directamente a la base de datos las 10 filas más recientes.
La razón por la que menciono esto es que los consejos sobre desempeño se vuelven vagos muy rápidamente. "Hazlo más rápido" no es útil. “Evitar ordenar todo cuando sólo necesito diez resultados” es útil. Un generador de perfiles te ayuda a llegar a ese nivel más preciso.
Recursos
Aquí están los enlaces oficiales de GitHub para cada herramienta:
+————+———————————————+ | Herramienta | Página oficial | +————+———————————————+ | fallar | https://github.com/astral-sh/ruff | | Negro | https://github.com/psf/black | | mipy | https://github.com/python/mypy | | pytest | https://github.com/pytest-dev/pytest | | compromiso previo | https://github.com/pre-commit/pre-commit | | py-espía | https://github.com/benfred/py-spy | +————+———————————————+
Tenga en cuenta también que muchos IDE modernos, como VSCode y PyCharm, tienen complementos para estas herramientas que brindan comentarios a medida que escribe, lo que las hace aún más útiles.
Resumen
La mayor fortaleza de Python (la velocidad a la que se puede pasar de la idea al código de trabajo) es también lo que hace que valga la pena invertir en herramientas disciplinadas. El lenguaje no le impedirá hacer suposiciones sobre las formas de los datos, dejar código muerto o escribir una función que funcione perfectamente en su entrada de prueba pero que falle en producción. Esa no es una crítica a Python. Es sólo la compensación que estás haciendo.
Las herramientas de este artículo ayudan a recuperar parte de esa seguridad sin sacrificar la velocidad.
El color negro controla el formateo para que nunca más tengas que pensar en ello. Ruff detecta los pequeños detalles sospechosos (importaciones no utilizadas, variables asignadas pero ignoradas) antes de que sobrevivan silenciosamente hasta convertirse en una publicación. Mypy te obliga a ser honesto acerca de la forma de los datos que realmente estás pasando, convirtiendo vagos fallos del tiempo de ejecución en comentarios tempranos y específicos. Pytest protege el comportamiento para que cuando cambies algo, sepas inmediatamente qué rompiste. La confirmación previa hace que todo esto sea automático, eliminando la mayor debilidad de cualquier proceso manual: recordar ejecutarlo.
Py-spy se encuentra ligeramente apartado de los demás. No lo ejecutas en cada confirmación. Lo buscas cuando algo correcto todavía es demasiado lento, cuando necesitas pasar de “hacerlo más rápido” a algo lo suficientemente preciso como para actuar.
Ninguna de estas herramientas sustituye a pensar detenidamente en su código. Lo que hacen es darles a los errores menos lugares donde esconderse. Y en un lenguaje tan permisivo como Python, eso vale bastante.
Tenga en cuenta que existen varias herramientas que pueden reemplazar cualquiera de las mencionadas anteriormente, por lo que si tiene un linter favorito que no es ruff, por ejemplo, no dude en usarlo en su flujo de trabajo.