Cuando un print() ya no alcanza y el stack trace no te dice lo suficiente, lo que necesitas es pausar el programa en mitad de su ejecución y mirar adentro. Eso es exactamente lo que hace pdb —el debugger estándar de Python— y lo que breakpoint() te da: un portal directo a ese entorno desde cualquier punto del código.
Antes de Python 3.7, insertar un breakpoint requería escribir import pdb; pdb.set_trace() en cada lugar. Feo, fácil de olvidar en un commit. La PEP 553 introdujo breakpoint() como builtin que hace exactamente lo mismo, pero con una diferencia importante en el diseño: delega el comportamiento al entorno a través de la variable PYTHONBREAKPOINT. Si defines PYTHONBREAKPOINT=0, todos los breakpoints del código se silencian sin tocar una línea. Si defines PYTHONBREAKPOINT=pudb.set_trace, usas pudb en lugar de pdb. La llamada en el código queda estable; el comportamiento es configurable desde fuera.
Eso importa en producción y en CI: puedes dejar breakpoint() en código de desarrollo y desactivarlo globalmente sin hacer diff.
Cuando el programa llega a un breakpoint(), se detiene y abre un prompt (Pdb). Ahí tienes acceso completo al estado del programa en ese frame: variables locales, la pila de llamadas, y la posibilidad de ejecutar cualquier expresión Python válida. No es un entorno de solo lectura —puedes mutar variables, llamar funciones, y luego continuar.
Lo que rompe a la gente que usa pdb por primera vez es confundir n (next) con s (step). n ejecuta la línea actual y pasa a la siguiente en el mismo frame, saltando el interior de cualquier función que se llame. s entra dentro de esa función. Si estás en una línea que llama a calcular_precio(item) y quieres ver qué pasa dentro, necesitas s, no n.
# archivo: tienda.py
def aplicar_descuento(precio: float, porcentaje: float) -> float:
# Deliberadamente con un bug sutil
factor = 1 - porcentaje # ← debería ser porcentaje / 100
return precio * factor
def calcular_total(carrito: list[dict]) -> float:
total = 0.0
for item in carrito:
precio_final = aplicar_descuento(item["precio"], item["descuento"])
total += precio_final
return total
def main():
carrito = [
{"nombre": "Teclado", "precio": 120.0, "descuento": 0.10},
{"nombre": "Monitor", "precio": 350.0, "descuento": 0.15},
{"nombre": "Mouse", "precio": 45.0, "descuento": 0.05},
]
breakpoint() # el programa se detiene aquí antes de calcular
total = calcular_total(carrito)
print(f"Total: ${total:.2f}")
if __name__ == "__main__":
main()
Cuando ejecutas python tienda.py, el programa llega a breakpoint() y abre el prompt:
> /ruta/tienda.py(25)main() -> total = calcular_total(carrito) (Pdb)
Navegando la sesión
l (list) muestra las líneas alrededor de donde estás. Sin argumentos, lista las 11 líneas centradas en la posición actual. Con l 1,40 ves desde la línea 1 hasta la 40.
p carrito evalúa la expresión e imprime el resultado. Funciona con cualquier expresión Python: p len(carrito), p carrito[0]["precio"] * 0.9. Si el objeto tiene una representación larga, pp carrito usa pprint y lo formatea correctamente.
w (where) imprime el traceback completo hasta el frame actual. Es tu brújula cuando estás varios niveles de llamadas abajo y no sabes cómo llegaste ahí.
b 10 pone un breakpoint en la línea 10 del archivo actual sin salir del debugger. b tienda.py:10 es más explícito cuando tienes múltiples archivos. b aplicar_descuento pone el breakpoint al inicio de esa función.
!factor = 0.9 —nótese el !— ejecuta esa asignación en el contexto del frame pausado. Esto te permite corregir variables en vuelo para probar hipótesis sin reiniciar el proceso.
Con el ejemplo anterior, la sesión típica sería:
(Pdb) s # entra en calcular_total
(Pdb) n # avanza línea a línea dentro del loop
(Pdb) p item
{'nombre': 'Teclado', 'precio': 120.0, 'descuento': 0.10}
(Pdb) s # entra en aplicar_descuento
(Pdb) p porcentaje
0.1
(Pdb) p factor # aún no ejecutada esta línea
*** NameError: name 'factor' is not defined
(Pdb) n # ejecuta la línea del factor
(Pdb) p factor
0.9 # correcto, 1 - 0.10
(Pdb) p precio * factor
108.0 # también correcto para este caso
(Pdb) c # continúa hasta el final
Aquí el bug resulta que no era un bug —el descuento ya venía expresado como fracción (0.10 = 10%). La sesión te habría revelado eso en segundos, sin un solo print adicional.
Errores que debes conocer
Error: usar n cuando quieres inspeccionar el interior de una función, y luego darte cuenta de que ya pasó y no puedes retroceder.
# ❌ Pasaste sobre la función sin entrar (Pdb) n # ejecutó aplicar_descuento completa, ya no puedes verla # ✅ Entras dentro de la función (Pdb) s # abre el frame de aplicar_descuento
pdb no tiene “deshacer” —la ejecución es hacia adelante. Si te saltas un frame con n, la única opción es reiniciar con r o terminar con q y volver a ejecutar.
Error: olvidar quitar breakpoint() antes de hacer commit, interrumpiendo tests y CI.
# ❌ Quedó en el código
def calcular_total(carrito):
breakpoint() # congela cualquier pipeline de CI
...
# ✅ Desactivado en entorno sin TTY
# En el comando de CI:
# PYTHONBREAKPOINT=0 pytest
PYTHONBREAKPOINT=0 hace que Python ignore silenciosamente todas las llamadas a breakpoint() en el proceso, sin modificar el código fuente.
Error: intentar usar p con expresiones que contienen comas sin paréntesis, interpretadas como múltiples argumentos.
# ❌ pdb interpreta esto como dos argumentos separados
(Pdb) p nombre, precio
# ✅ wrap en paréntesis para que sea una tupla
(Pdb) p (nombre, precio)
('Teclado', 120.0)
El parser de pdb divide por comas al igual que lo haría un intérprete de comandos; los paréntesis fuerzan que sea una expresión Python única.
N° 163