Los prints y los logs de la lección anterior responden preguntas que formulaste antes de ejecutar. Pero a veces necesitas conversar con el programa mientras corre: pararlo en una línea exacta, preguntar por cualquier variable — incluso las que no se te ocurrió vigilar —, avanzar una línea, entrar dentro de una llamada, y decidir el siguiente paso a la vista de lo anterior. Eso es un depurador interactivo. Python trae uno de serie, pdb, y en esta lección lo usarás para perseguir un bug de stock dentro de vender(), aprenderás a abrirlo justo donde un test de pytest falló, y lo compararás con el depurador visual de VS Code. Con él se completa la red de seguridad de Papyrus.
Contenido
breakpoint(): pausar el mundo- Los comandos esenciales (con mnemotecnia)
- Caso práctico: persiguiendo el bug del stock dentro de
vender() - Post-mortem:
pytest --pdbse abre donde el test falló - El depurador de VS Code: lo mismo, con ventanas
- Buenas prácticas (y cómo no dejar una bomba en producción)
- Cierre del módulo: la red de seguridad completa
breakpoint(): pausar el mundo
Desde Python 3.7, basta una función incorporada:
def vender(catalogo, titulo, unidades):
libro = obtener_libro(catalogo, titulo)
breakpoint() # ← la ejecución se PAUSA aquí
...Al llegar a esa línea, el programa se detiene y la terminal se convierte en una consola interactiva dentro del programa vivo, con acceso a todas las variables locales en ese instante:
> .../papyrus/almacen.py(57)vender() -> if not isinstance(unidades, int): (Pdb) libro.stock 4 (Pdb) unidades 4
El prompt (Pdb) acepta comandos del depurador y expresiones Python normales (libro.stock, unidades * libro.precio, lo que quieras). breakpoint() es en realidad un envoltorio configurable de import pdb; pdb.set_trace(), la forma clásica que verás en código antiguo; hoy se prefiere breakpoint() por ser más corta y — como verás al final — desactivable desde fuera. La línea que muestra -> es la siguiente que se ejecutará: estás justo antes de ella, a tiempo de mirarlo todo.
Los comandos esenciales (con mnemotecnia)
Con diez comandos cubres el 95 % de las sesiones. La letra es la inicial inglesa — esa es toda la mnemotecnia que necesitas:
| Comando | Inglés (mnemotecnia) | Qué hace |
|---|---|---|
l |
list | Muestra el código alrededor de la línea actual (ll = la función entera) |
n |
next | Ejecuta la línea actual sin entrar en sus llamadas |
s |
step | Ejecuta la línea actual entrando en la función que llama |
c |
continue | Reanuda hasta el siguiente breakpoint (o el final) |
b 62 |
break | Pone un punto de ruptura en la línea 62 (b solo: lista todos) |
p expr |
Evalúa e imprime una expresión | |
pp expr |
pretty-print | Como p, pero formatea bonito (dicts y objetos grandes) |
w |
where | Muestra la pila de llamadas y tu posición en ella |
u / d |
up / down | Sube/baja un marco en la pila (¡puedes inspeccionar al llamador!) |
q |
quit | Aborta la ejecución y sale del depurador |
La pareja que más confunde al principio: n pasa por encima, s se mete dentro. Si la línea actual es precio = libro.precio_final(socio), con n obtienes precio ya calculado; con s apareces en la primera línea de precio_final para verla trabajar por dentro. Y w/u/d son el traceback de 09-05 hecho navegable: la misma pila, pero puedes pasearte por ella con las variables vivas de cada marco.
Caso práctico: persiguiendo el bug del stock dentro de vender()
Tras un refactor en almacen.py, la suite canta:
FAILED tests/test_almacen.py::test_vender_todo_el_stock_deja_cero E papyrus.errores.StockInsuficienteError: Stock insuficiente de 'La Odisea': pedidas 4, disponibles 4
El mensaje ya es sospechoso: ¿"insuficiente" con 4 pedidas y 4 disponibles? Vender exactamente todo el stock es legal (el caso límite de 09-01). Reproducimos con un caso mínimo y plantamos un breakpoint() a la entrada de vender, tras las validaciones:
# minimo.py
from papyrus.almacen import vender
from papyrus.modelos import Libro
catalogo = {"La Odisea": Libro("La Odisea", 12.50, 4)}
vender(catalogo, "La Odisea", 4) # legal: justo todo el stockSesión completa (tus comandos tras (Pdb); anotada a la derecha):
$ python minimo.py > .../papyrus/almacen.py(60)vender() -> if libro.stock <= unidades: (Pdb) p libro.stock, unidades # ¿qué compara exactamente? (4, 4) (Pdb) p libro.stock <= unidades # evaluamos la condición A MANO True # ← ¡va a lanzar la excepción! (Pdb) ll # veamos la función entera 55 def vender(catalogo, titulo, unidades): ... 60 -> if libro.stock <= unidades: 61 raise StockInsuficienteError(titulo, unidades, libro.stock) (Pdb) n # confirmamos: next ejecuta el if... > .../papyrus/almacen.py(61)vender() -> raise StockInsuficienteError(titulo, unidades, libro.stock) (Pdb) q # visto: no hace falta más
Diagnóstico en cuatro comandos: la condición dice <= donde el contrato exige < — con stock 4 y 4 unidades pedidas no falta stock; falta cuando piden más de lo disponible. Es el clásico off-by-one de los operadores de comparación. Fíjate en la jugada clave: p libro.stock <= unidades evaluó la condición sospechosa antes de ejecutarla — algo que ningún print retrospectivo puede hacer. Se corrige <= → <, la suite vuelve a verde (el test que falló ya era, él mismo, la regresión que protege este límite), y de propina puedes comprobar con p que tras la venta libro.stock == 0.
Post-mortem: pytest --pdb se abre donde el test falló
En el caso anterior editamos el código para plantar el breakpoint(). Hay un atajo mejor cuando el que falla es un test:
pytest --pdb # al primer fallo, abre pdb EN el punto del fallo
pytest -x --pdb # combinado con -x: para y depura el primer rojoEs una autopsia (post-mortem): el test ya falló, pero pdb te deja dentro del cadáver con todas las variables del momento del fallo intactas — el catálogo del test, la excepción, todo. Desde ahí, w te enseña la pila, u te sube del test a vender, y p interroga lo que haga falta. Sin editar una sola línea. Para código que no es un test, existe el equivalente python -m pdb minimo.py (ejecuta bajo el depurador desde el principio y entra en modo post-mortem si revienta). El flujo profesional con pytest es: pytest detecta, pytest --lf --pdb te planta en el escenario del crimen.
El depurador de VS Code: lo mismo, con ventanas
Todo lo anterior existe en versión visual. En VS Code (con la extensión de Python): haces clic en el margen izquierdo de una línea (aparece un punto rojo: un breakpoint visual, el b de pdb), lanzas con F5 (o "Debug Test" sobre un test concreto), y cuando la ejecución se detiene tienes: panel de variables (todos los locales, sin pedirlos con p), watch (expresiones vigiladas que se reevalúan a cada paso), la pila de llamadas clicable (el w/u/d), y botones para step over (n), step into (s) y continue (c). Los conceptos son idénticos — por eso aprendimos pdb primero: quien entiende n/s/c entiende cualquier depurador de cualquier lenguaje.
¿Cuál usar? No es religión, es contexto:
| Situación | Mejor opción |
|---|---|
| Exploras un bug complejo en tu máquina, con calma | VS Code: ver todos los locales a la vez acelera la formación de hipótesis |
| Estás en un servidor por SSH, sin entorno gráfico | pdb: siempre está ahí, no necesita nada |
| Un test concreto falla y quieres el contexto exacto | pytest --pdb (o "Debug Test" en VS Code) |
| Sesión rápida: una condición sospechosa, dos preguntas | breakpoint() + pdb: plantado en 5 segundos |
| Quieres evaluar expresiones arbitrarias sobre la marcha | Empate: consola (Pdb) o la Debug Console de VS Code |
Buenas prácticas (y cómo no dejar una bomba en producción)
- Nunca dejes un
breakpoint()en código que entregas. Es la versión nuclear del print olvidado: el programa de otro se detendrá esperando una consola que quizá no existe. Antes de dar por cerrada una sesión de depuración, busca y elimina:grep -rn "breakpoint()" papyrus/(con Grep/Select-String en tu entorno). - La red de seguridad externa: la variable de entorno
PYTHONBREAKPOINT=0hace que todos losbreakpoint()se ignoren sin tocar código — se usa como cinturón en entornos de producción. Que exista no te exime de limpiar; te salva del día que no limpiaste. - Deja el depurador mejor que lo encontraste: si durante la sesión descubriste un valor que "debería ser imposible", conviértelo en
asserto en test antes de cerrar. La sesión de pdb es efímera; el test queda. - No depures lo que un log ya responde. Abrir pdb para saber "¿llegó a ejecutarse esta función?" es matar moscas a cañonazos: eso es un
logger.debug(09-05). El depurador es para preguntas que requieren el estado completo y la capacidad de avanzar. - En pdb, cuidado con las variables que se llaman como comandos:
p limprime la variablel... si existe; escribirla secas lista código. Si tu variable se llamanoc, usap n— y mejor: no llamesna una variable (M1 ya te pedía nombres con significado).
Cierre del módulo: la red de seguridad completa
Mira la red que Papyrus ha tejido entre los módulos 8 y 9 — cuatro capas, cada una cazando lo que se le escapa a la anterior:
| Capa | Herramienta | Qué caza | Cuándo actúa |
|---|---|---|---|
| Tipos | mypy + type hints (08-01) |
Contratos de forma: un str donde va un int |
Antes de ejecutar |
| Comportamiento | Suite de tests: pytest (09-01→09-04) |
Contratos de valor: 12.35, no 12.36 | En cada cambio, en segundos |
| Diagnóstico en vivo | pdb / VS Code (09-06) |
El porqué de un fallo ya reproducido | Durante la caza del bug |
| Memoria | logging a papyrus.log (M7, 09-05) |
Lo que pasó cuando no estabas mirando | Siempre, también en producción |
Ninguna capa sustituye a las demás: mypy no sabe que el descuento es del 5 %, los tests no explican por qué fallan, pdb no protege el futuro y los logs no impiden nada. Juntas, convierten "cambiar una línea de Papyrus" — la pregunta que abrió este módulo — en un acto sin miedo: si algo se rompe, alguna capa gritará, y sabrás dónde, cuándo y por qué.
Errores Comunes y Consejos
- Confundir
nysy "perderte" dentro de una función de la biblioteca estándar tras unsde más: no entres en pánico —r(return) ejecuta hasta salir de la función actual, yute recuerda desde dónde llamaste. - Pulsar Enter sin comando: pdb repite el último comando. Es utilísimo para avanzar con
n,n, Enter, Enter... y desconcertante si no lo sabes (¿por qué avanza si no escribí nada?). - Depurar la versión equivocada del código: si editas un fichero mientras pdb está abierto, la sesión sigue ejecutando el código antiguo (ya cargado). Sal, reejecuta y vuelve a entrar.
- Usar
qesperando que el programa continúe:qaborta la ejecución entera (verásBdbQuit). Para "suéltame y sigue normal", el comando esc. - Abusar del depurador como sustituto de tests: si cada cambio requiere una sesión de pdb para convencerte de que funciona, te faltan tests. El depurador diagnostica; la suite garantiza.
- Consejo: en
pytest --pdb, tu primer comando casi siempre debería serw(¿dónde estoy?) y el segundoppsobre la estructura de datos central (aquí,pp catalogo). Orientarte antes de moverte ahorra la mitad de la sesión.
Ejercicios
Ejercicio 1
Sin ejecutar nada, escribe la secuencia exacta de comandos pdb para esta misión: estás parado con breakpoint() en la primera línea de cobrar_a_socio (en caja.py), que más abajo llama a vender. Quieres: (a) ver la función entera, (b) avanzar hasta la línea de la llamada a vender (dos líneas más abajo), (c) entrar dentro de vender, (d) una vez dentro, ver la pila para confirmar desde dónde llegaste, (e) imprimir con formato el catálogo completo, (f) continuar la ejecución normal hasta el final.
Ejercicio 2
Un compañero te pasa este diff como "arreglo" tras una sesión de depuración. Señala los dos problemas que no deberían llegar al repositorio y explica qué haría cada uno en producción:
def reponer(catalogo, titulo, unidades):
libro = obtener_libro(catalogo, titulo)
+ breakpoint()
if not isinstance(unidades, int) or unidades <= 0:
raise ValueError(f"Unidades inválidas: {unidades}")
+ print(f"DEBUG {titulo=} {unidades=}")
libro.stock += unidadesEjercicio 3
El test test_cierre_de_caja_suma_solo_filas_validas (09-03) falla tras un refactor del pipeline de generadores. Describe, paso a paso, cómo usarías pytest para llegar al punto del fallo sin editar ningún fichero, y qué tres primeras preguntas (expresiones para p/pp) harías una vez dentro.
Soluciones
Ejercicio 1
(Pdb) ll # (a) la función entera, con la flecha en la línea actual (Pdb) n # (b) avanza una línea... (Pdb) n # ...y otra: la flecha apunta ahora a la llamada a vender (Pdb) s # (c) step: entra DENTRO de vender (Pdb) w # (d) la pila: jornada.py → cobrar_a_socio → vender (tú estás abajo) (Pdb) pp catalogo # (e) el catálogo, formateado (Pdb) c # (f) continue: suelta el programa hasta el final
(En (b) también vale b 33 + c si conoces el número de línea: plantar un break y continuar hasta él — más cómodo cuando hay diez líneas por medio.)
Ejercicio 2 — Problema 1: el breakpoint(). En producción, reponer se detendría esperando entrada de un depurador que nadie atiende: Papyrus congelado en plena reposición (mitigable con PYTHONBREAKPOINT=0 en el entorno, pero la bomba no debe embarcar). Problema 2: el print de depuración, que ensucia la salida estándar de cualquier programa que use el paquete (imagina el cierre de caja imprimiendo cientos de líneas DEBUG). Si esa traza tiene valor permanente, su forma correcta es logger.debug("reponer: %r +%d", titulo, unidades) — silenciosa por defecto, activable cuando haga falta (09-05).
Ejercicio 3 — Pasos: (1) pytest --lf para confirmar que solo ese test falla y ver el mensaje; (2) pytest --lf --pdb — al fallar, pdb se abre en el punto exacto del fallo con el estado intacto; (3) orientación: w para ver la pila (¿falló en el assert del test o dentro de cierre_de_caja? con u/d te mueves al marco interesante). Tres primeras preguntas típicas: p ruta.read_text() (¿qué contenía exactamente el CSV del test?), pp sobre la estructura intermedia del pipeline si estás en un marco de cierre_de_caja (por ejemplo, materializar p list(parsear(leer_ventas(ruta))) para ver qué filas sobrevivieron al parseo — ojo: consumir un generador en pdb lo agota para el resto de la ejecución, legítimo en una autopsia), y p total o la expresión del assert para ver el valor que se comparó. Con eso sabrás si el refactor pierde filas, parsea mal los importes o filtra de más.
Conclusión
El módulo 9 respondió la pregunta con la que el módulo 8 nos dejó en vilo. Ahora, cuando Ana cambie una línea, no cruza los dedos: ejecuta pytest y en segundos sabe si los cuatro precios de socio siguen siendo 12.35, 9.83, 15.71 y 20.75 (09-01, 09-02, 09-03). Los contratos de vender — el importe con IVA, el stock intacto tras un StockInsuficienteError — dejaron de ser promesas y son tests que fallan en rojo si alguien las rompe. Los cupones nacieron al revés y mejor: test primero, código mínimo después, refactor sin miedo (09-04). Y cuando algo se rompe igualmente, hay método — reproducir, aislar, corregir, proteger con una regresión (09-05) — y hay lupa: breakpoint(), cuatro comandos de pdb y la condición sospechosa evaluada en vivo antes de ejecutarse (09-06). Con mypy vigilando los tipos, la suite vigilando el comportamiento, el depurador para el porqué y papyrus.log como memoria, Papyrus tiene por fin una red de seguridad completa. Pero levanta la vista: todo esto — catálogo, ventas, cupones, cierres — vive en un solo sitio, el ordenador de Ana, y solo ella puede usarlo. Luis querría consultar el stock desde casa; Júlia, reservar un libro desde el móvil. La siguiente frontera no es hacer que Papyrus funcione mejor, sino abrirlo al mundo: convertirlo en una aplicación web. Ese es el módulo 10.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
