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

  1. breakpoint(): pausar el mundo
  2. Los comandos esenciales (con mnemotecnia)
  3. Caso práctico: persiguiendo el bug del stock dentro de vender()
  4. Post-mortem: pytest --pdb se abre donde el test falló
  5. El depurador de VS Code: lo mismo, con ventanas
  6. Buenas prácticas (y cómo no dejar una bomba en producción)
  7. 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 print 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 stock

Sesió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 rojo

Es 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=0 hace que todos los breakpoint() 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 assert o 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 l imprime la variable l... si existe; escribir l a secas lista código. Si tu variable se llama n o c, usa p n — y mejor: no llames n a 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 n y s y "perderte" dentro de una función de la biblioteca estándar tras un s de más: no entres en pánico — r (return) ejecuta hasta salir de la función actual, y u te 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 q esperando que el programa continúe: q aborta la ejecución entera (verás BdbQuit). Para "suéltame y sigue normal", el comando es c.
  • 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 ser w (¿dónde estoy?) y el segundo pp sobre 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 += unidades

Ejercicio 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados