Saltar al contenido

Indice / Herramientas / 0.1

Depurar en VS Code: breakpoints y la consola

Un programa que no anda no se arregla mirandolo fijo. Se para en el medio, se abre y se mira que tiene adentro. Esta leccion es la herramienta que hace eso, y es la que mas tiempo te va a ahorrar en toda la materia.

Antes: nada, se puede empezar por aca Despues: 0.2 Positron: el IDE pensado para datos

Para que sirve

Cuando algo da mal, casi todo el mundo hace lo mismo: agrega un print, vuelve a correr, mira, agrega otro print, vuelve a correr. Funciona, y por eso cuesta abandonarlo. Pero tiene tres costos que se pagan todos los dias:

El depurador da vuelta la relacion: en vez de vos preguntandole cosas al programa de a una y por adelantado, el programa se queda quieto y vos le preguntas lo que quieras, en el orden que quieras, con los datos de verdad cargados en memoria. Lo vas a usar cada vez que un TypeError no diga en que columna, que es casi siempre.

ELI5Explicado facil

Imaginate que se te pincho la bici y para revisarla tenes que andar. Vas pedaleando y gritas «¿ahora anda?», «¿y ahora?». Eso es llenar el codigo de print.

El depurador es bajarte y frenar la rueda con la mano. La bici queda quieta en la posicion exacta en la que fallaba, y podes tocar todo: girar el pedal despacio, mirar la cadena, apretar el freno. Cuando terminas, la soltas y sigue andando desde donde estaba.

Lo minimo que hay que saber

Son cinco teclas y un click. Con eso alcanza para el 90 % de los casos; el resto de la leccion es sacarle punta.

El juego completo de controles
GestoQue haceCuando
Click en la canaleta (o F9)Pone o saca un breakpoint: un punto rojo a la izquierda del numero de lineaDonde queres que frene
F5Iniciar la depuracion, o continuar hasta el proximo breakpointArrancar, y saltar de parada en parada
F10Step Over: ejecuta la linea entera y frena en la siguienteEl paso por defecto
F11Step Into: si la linea llama a una funcion tuya, entra adentroCuando sospechas de la funcion, no de la linea
Shift+F11Step Out: termina la funcion actual y vuelve a quien la llamoCuando entraste de mas
Shift+F5DetenerAl terminar
Step Over y Step Into no compiten: contestan preguntas distintas

F10 pregunta «¿que hace esta linea?» y te muestra el resultado. F11 pregunta «¿por que esta linea hace eso?» y te mete adentro. Si ya sabes que la funcion anda bien, F10. Si el resultado de la funcion es justamente lo que no entendes, F11.

El ejemplo minimo

Este es el programa que vamos a recorrer. Calcula el promedio de las tres notas del primer estudiante, sin pandas todavia: solo la biblioteca estandar.

EjemploEl script que vamos a depurar (todavia sin pandas)
import csv

def promedio(valores):
    total = 0.0
    for v in valores:
        total = total + float(v)
    return total / len(valores)

with open("estudiantes_notas.csv", encoding="utf-8") as f:
    filas = list(csv.DictReader(f))

fila = filas[0]
notas = [fila["nota_1"], fila["nota_2"], fila["nota_3"]]
print(fila["estudiante"], promedio(notas))
Karina Acosta 7.666666666666667

Lo mismo, paso a paso

Nueve pasos: poner el breakpoint, arrancar, avanzar linea por linea, entrar en la funcion y preguntarle cosas a la consola con el programa detenido. Mira las tres zonas a la vez - el codigo, las variables y la consola -, porque aprender a mirarlas juntas es la habilidad.

Paso a pasoUna corrida completa con el depurador: parar, mirar y avanzar
  1. Click en la canaletaUn click en el espacio a la izquierda del numero 12 deja un punto rojo. Eso es un breakpoint: la orden de frenar justo antes de ejecutar esa linea. Todavia no corrio nada.
    fila = filas[0]
  2. F5 - IniciarEl programa arranca y se detiene solo al llegar al breakpoint. La flecha marca la linea 12: esta por ejecutarse, todavia no se ejecuto. Las lineas 1 a 10 ya corrieron, y por eso filas ya tiene las 40 filas del archivo.
    filas = list(csv.DictReader(f))
  3. F10 - Step OverStep Over ejecuta la linea 12 entera y frena en la 13. Aparece fila en el panel: es un diccionario, y todos sus valores son texto, porque csv.DictReader no convierte tipos. Ahi ya hay una pista de lo que va a pasar despues.
    fila = filas[0]
  4. F10 - Step OverOtro Step Over y ya esta armada la lista notas. Se ve clarito que son tres cadenas, no tres numeros: llevan comillas.
    notas = [fila["nota_1"], fila["nota_2"], fila["nota_3"]]
  5. F11 - Step IntoAca esta la diferencia que importa. Step Over habria ejecutado promedio(notas) de un saque y mostrado el resultado. Step Into entra adentro de la funcion: la flecha salta a la linea 4 y el panel cambia, porque ahora muestra las variables de la funcion, no las del programa principal.
    def promedio(valores):
  6. F10 x2Dos pasos mas: total ya existe y vale 0.0, y el for tomo el primer valor. v es la cadena '9.7', con comillas. Que el panel muestre las comillas es la forma mas rapida de distinguir un numero de un texto que parece numero.
    for v in valores:
  7. Consola de depuracionCon la ejecucion detenida, la consola es un interprete parado adentro del programa: lo que se escribe ahi se evalua con las variables tal como estan en este instante. Aca se prueba la conversion antes de que el programa la haga.
    float(v)
  8. F10 x3Tres vueltas del for despues, total llego a 23.0. Fijate en el valor intermedio: 9.7 + 4.7 no dio 14.4 sino 14.399999999999999. No es un error del depurador, es como funcionan los flotantes, y verlo aca ahorra un susto mas adelante.
    total = total + float(v)
  9. F5 - ContinuarContinuar deja correr hasta el proximo breakpoint o hasta el final. Como no hay otro, el programa termina e imprime. El promedio salio bien porque esta fila no tiene ningun valor raro: el problema aparece con el archivo entero, y de eso se trata la animacion siguiente.
    print(fila["estudiante"], promedio(notas))
Las comillas del panel de variables no son decoracion

Cuando el panel muestra '9.7' con comillas, es un texto. Cuando muestra 9.7 sin comillas, es un numero. Esa diferencia de dos caracteres es la causa de la mitad de los errores de esta materia, y en el panel se ve de un vistazo. En un print no se ve: print("9.7") y print(9.7) escriben lo mismo en la pantalla.

Sobre datos de verdad

El programa de arriba anda. Ahora el mismo calculo, con pandas, sobre las 40 filas del archivo:

EjemploEl mismo calculo con pandas, sobre el archivo entero: falla
import pandas as pd

notas = pd.read_csv("estudiantes_notas.csv")
columnas = ["nota_1", "nota_2", "nota_3"]

try:
    notas["promedio"] = notas[columnas].mean(axis=1)
    print(notas["promedio"].round(2).head())
except TypeError as error:
    print("TypeError:", error)

print()
print("El mensaje no dice que columna, ni que fila, ni que valor.")
print("Dice que en algun lado se intento sumar un float con un str.")
TypeError: unsupported operand type(s) for +: 'float' and 'str'

El mensaje no dice que columna, ni que fila, ni que valor.
Dice que en algun lado se intento sumar un float con un str.

Este es el error tipico de la materia, y el mensaje es un ejemplo perfecto de lo inutil que puede ser un mensaje correcto. Dice la verdad - alguien sumo un float con un str - y no dice nada de lo que necesitas: que columna, que fila, que valor. Con tres columnas y 40 filas hay 120 candidatos.

Encontrarlo con el depurador

Paso a pasoUn error real: de TypeError a las dos filas culpables, sin agregar un solo print
  1. EjecutarEl programa corre y revienta. El mensaje es cierto pero inutil: no dice que columna, ni que fila, ni que valor. En algun lado, entre 40 filas y 3 columnas, hay un texto donde deberia haber un numero.
    notas["promedio"] = notas[columnas].mean(axis=1)
  2. Breakpoint en la 6 + F5Un breakpoint en la linea que falla y F5. El programa se detiene justo antes de romperse, con todo cargado. El panel dice que notas tiene 40 filas y 5 columnas, pero no dice de que tipo es cada una: para eso hay que preguntar.
    notas = pd.read_csv("estudiantes_notas.csv")
  3. Consola de depuracionPrimera pregunta, y casi siempre la que resuelve el caso: de que tipo quedo cada columna. nota_1 y nota_2 son float64. nota_3 quedo str. Esa sola linea explica el error entero: pandas intento sumar un float con un texto.
    notas.dtypes
  4. Consola de depuracionSegunda pregunta: que valor es el que no es un numero. Ordenar los valores distintos pone los raros primero, y ahi esta el culpable: un guion suelto. Como el guion no esta en la lista de ausentes que read_csv reconoce por defecto, la columna entera se leyo como texto.
    sorted(notas["nota_3"].dropna().unique())[:4]
  5. Consola de depuracionTercera pregunta: cuantas filas y cuales. Dos filas de cuarenta alcanzaron para cambiar el tipo de la columna entera y voltear el programa. Ahora el problema tiene nombre y apellido, y se puede ir a mirar el archivo.
    notas.loc[notas["nota_3"] == "-", "legajo"].tolist()
  6. Consola de depuracionAca esta lo que casi nadie usa: en la consola no solo se puede preguntar, tambien se puede hacer. Volvemos a leer el archivo declarando que el guion significa ausente, sin cerrar el depurador ni reiniciar nada.
    notas = pd.read_csv("estudiantes_notas.csv", na_values=["-"])
  7. Consola de depuracionY se prueba ahi mismo la linea que fallaba. Anda. Recien ahora sabemos que el arreglo es el correcto, antes de tocar el archivo: lo probamos sobre los datos de verdad.
    notas[columnas].mean(axis=1).round(2).head()
  8. Arreglar y sacar el breakpointUltimo paso, el que se olvida: llevar el arreglo al archivo, sacar el breakpoint y correr normal. El guion ahora se lee como ausente, nota_3 es float64 y el promedio sale. Cinco preguntas en la consola, ningun print agregado y ninguno que borrar despues.
    notas = pd.read_csv("estudiantes_notas.csv", na_values=["-"])

Cinco preguntas y el caso esta cerrado. Lo importante no es el resultado sino el orden, que es siempre el mismo y sirve para cualquier error de tipos:

  1. notas.dtypes - ¿de que tipo quedo cada columna?
  2. sorted(col.dropna().unique())[:4] - ¿cual es el valor raro?
  3. (col == "-").sum() - ¿cuantas filas?
  4. df.loc[col == "-", "legajo"] - ¿cuales?
  5. Probar el arreglo ahi mismo, antes de tocar el archivo.

Las tres primeras, escritas como programa, imprimen esto:

EjemploLo que se escribe en la consola de depuracion para encontrar la causa
import pandas as pd

notas = pd.read_csv("estudiantes_notas.csv")

# Cada una de estas lineas es una expresion que se tipea en la consola con la
# ejecucion detenida. Aca van juntas para poder mostrar sus salidas.
print(notas.dtypes)

print()
print("primeros valores distintos de nota_3, ordenados:")
print(sorted(notas["nota_3"].dropna().unique())[:4])

print()
print("cuantas filas tienen el guion:", int((notas["nota_3"] == "-").sum()))
legajo            str
estudiante        str
nota_1        float64
nota_2        float64
nota_3            str
dtype: object

primeros valores distintos de nota_3, ordenados:
['-', '-1.0', '3.2', '3.4']

cuantas filas tienen el guion: 2

Y la cuarta le pone nombre y apellido al problema:

EjemploLas dos filas culpables, en una sola expresion
import pandas as pd

notas = pd.read_csv("estudiantes_notas.csv")

culpables = notas["nota_3"] == "-"
print(notas.loc[culpables].to_string(index=False))

print()
print("Esa misma condicion sirve como breakpoint condicional:")
print('   notas["nota_3"][i] == "-"')
print("y el depurador se detiene solo en esas filas.")
legajo      estudiante  nota_1  nota_2 nota_3
 L2621 Zacarías Juárez     3.5     5.5      -
 L2629  Zacarías Gómez     5.7     5.1      -

Esa misma condicion sirve como breakpoint condicional:
   notas["nota_3"][i] == "-"
y el depurador se detiene solo en esas filas.
Dos filas de cuarenta rompieron todo

Un solo - en una columna alcanza para que read_csv la lea entera como texto, porque el guion no esta en la lista de tokens de ausente que pandas reconoce por defecto (NaN, N/A y null si estan; el guion no). Es la trampa que mas veces vas a encontrar en esta materia, y la vas a ver en detalle en la leccion 5.4.

Con el arreglo puesto, el programa anda y ademas queda declarado en el codigo que el guion significa ausente:

EjemploEl arreglo, y la verificacion de que quedo arreglado
import pandas as pd

notas = pd.read_csv("estudiantes_notas.csv", na_values=["-"])
columnas = ["nota_1", "nota_2", "nota_3"]

print("dtype de nota_3:", notas["nota_3"].dtype)
print("ausentes en nota_3:", int(notas["nota_3"].isna().sum()))

notas["promedio"] = notas[columnas].mean(axis=1)
print()
print(notas[["legajo", "estudiante", "promedio"]].head(3).round(2).to_string(index=False))
dtype de nota_3: float64
ausentes en nota_3: 5

legajo     estudiante  promedio
 L2601  Karina Acosta      7.67
 L2602 Santiago Ramos      4.67
 L2603     Carla Luna      5.17

La consola de depuracion

Si de toda la leccion te queda una sola cosa, que sea esta. La Consola de depuracion (Ctrl+Shift+Y) no es un panel de mensajes: es un interprete de Python parado adentro de tu programa, en la linea exacta donde esta detenido y con todas sus variables vivas.

Eso cambia como se trabaja. Lo que en un cuaderno de Jupyter harias en una celda nueva, aca lo haces sobre los datos reales del programa, en el punto exacto del problema, sin volver a cargar nada:

Lo que se puede escribir con el programa detenido
Se escribeQue devuelve
notas.dtypesEl tipo de cada columna, ahora
notas.head(3)Las primeras filas, formateadas
notas["nota_3"].unique()Los valores distintos, para cazar el raro
len(filas)Cualquier expresion de Python, no solo variables
notas = pd.read_csv(...)Tambien se puede hacer: probar el arreglo sin reiniciar
ELI5Explicado facil

Es como poder pausar una pelicula y meterte adentro de la escena a revisarle los bolsillos a los personajes. La pelicula queda congelada, vos revisas todo lo que quieras, y cuando le das play sigue como si nada.

Tres cosas mas que rinden mucho

Breakpoint condicional

Frenar en cada una de las 40 vueltas de un bucle para encontrar la fila mala es insoportable. Click derecho en la canaleta → Add Conditional Breakpoint, y se escribe una expresion de Python: el programa frena solo cuando esa expresion da verdadero.

Con el caso de arriba, la condicion seria fila["nota_3"] == "-": de 40 vueltas, frena en 2. Es la diferencia entre usar el depurador y odiarlo.

Logpoint

Click derecho → Add Logpoint. Es un breakpoint que no frena: escribe un mensaje en la consola y sigue. Sirve para lo mismo que un print, con dos ventajas: no toca el archivo (asi que no hay nada que borrar despues) y se pone o se saca sin volver a correr.

Watch

En el panel Watch se anotan expresiones que se recalculan solas en cada paso. Poner ahi notas["nota_3"].dtype y avanzar es ver el tipo de la columna cambiar en vivo, sin escribirlo de nuevo cada vez.

Sin IDE tambien se puede: breakpoint()

Escribir breakpoint() en una linea cualquiera y correr el programa normal, desde la terminal, abre el depurador de la biblioteca estandar (pdb) en ese punto. Los comandos son n (siguiente), s (entrar), c (continuar), p variable (imprimir) y q (salir). Es la misma idea sin ventanas, y funciona en un servidor por SSH donde no hay VS Code.

Donde se rompe

FileNotFoundError: 'estudiantes_notas.csv'

Causa: el depurador arranca el programa desde la carpeta que tenes abierta en VS Code, no desde la carpeta del archivo .py. Si el CSV esta al lado del script pero abriste una carpeta de mas arriba, el nombre pelado no resuelve.

Arreglo: abri en VS Code la carpeta donde estan los datos, o agrega "cwd" en la configuracion de launch.json. Es el error numero uno de esta materia, porque todos los ejemplos abren los archivos por nombre pelado.

ModuleNotFoundError: No module named 'pandas'

Causa: el depurador esta usando otro interprete de Python que el que usas en la terminal - tipicamente el del sistema en vez del entorno virtual.

Arreglo: Ctrl+Shift+P → Python: Select Interpreter, y elegi el mismo que usas para correr. El interprete elegido se ve abajo a la derecha, en la barra de estado.

(sin error) el programa no frena en el breakpoint

Causa: casi siempre, esa linea no se ejecuta. Esta adentro de un if que da falso, de una funcion que nadie llama, o despues de un return.

Arreglo: pone otro breakpoint en la primera linea del archivo. Si en ese si frena, el problema no es el depurador: es que tu idea de por donde pasa el programa esta equivocada - que ya es un hallazgo.

(sin error) F11 no entra en la funcion

Causa: la funcion es de una biblioteca, no tuya. Por defecto el depurador de Python trae justMyCode activado y saltea el codigo de terceros.

Arreglo: casi siempre no querias entrar en pandas. Si de verdad hace falta, se pone "justMyCode": false en launch.json.

(sin error) el panel de Variables no muestra el DataFrame entero

Causa: muestra un resumen, no los datos. Es a proposito: dibujar 500 000 filas colgaria el editor.

Arreglo: pedile lo que necesitas por la consola (notas.head(10), notas.describe()). Y si mirar tablas grandes es tu tarea de todos los dias, mira la leccion 0.2: hay un IDE con una planilla de verdad para eso.

Aca hay que elegir

print o depurador

No es que uno este mal. Es que sirven para cosas distintas y conviene saber cual:

  • print - cuando ya sabes que queres mirar y lo queres ver en cada vuelta de un bucle largo, o cuando el programa corre donde no podes conectarte. Un print deja rastro escrito; el depurador no.
  • Depurador - cuando no sabes que mirar. Que es la situacion normal cuando algo falla y no entendes por que. Cinco preguntas en la consola valen veinte corridas con print.
  • Logpoint - cuando queres el rastro escrito del print pero sin ensuciar el archivo.

La regla practica: si podes nombrar la variable que queres ver, print alcanza. Si tu pregunta empieza con «pero por que…», es depurador.

Recetario

Depuracion en VS Code
Gesto o expresionQue haceCuando
F9Pone o saca el breakpoint de la linea actualSin soltar el teclado
F5Inicia, o continua hasta el proximo breakpointArrancar y saltar
Ctrl+F5Corre sin depurarCuando solo queres el resultado
F10 / F11 / Shift+F11Step Over / Into / OutAvanzar con control
Ctrl+Shift+DAbre el panel Run and DebugVer variables, pila y watch
Ctrl+Shift+YAbre la consola de depuracionPreguntar con el programa detenido
Click derecho → Conditional BreakpointFrena solo si la expresion da verdaderoBuscar una fila entre miles
Click derecho → LogpointEscribe un mensaje y sigue, sin tocar el archivoprint sin dejar rastro
Panel WatchRecalcula una expresion en cada pasoSeguir un valor que cambia
"justMyCode": falseDeja entrar en el codigo de las bibliotecasRara vez; casi nunca es lo que queres
"cwd" en launch.jsonFija desde que carpeta corre el programaCuando los read_csv no encuentran nada
breakpoint()Abre pdb en esa linea, sin IDEPor SSH, o sin VS Code

Ponete a prueba

1. ¿Que hace exactamente un breakpoint?Nivel 1 · Recordar
2. El programa esta detenido en una linea que dice total = suma(valores). Queres ver que pasa adentro de suma. ¿Que tecla?Nivel 1 · Recordar
3. En el panel de Variables se ve v con el valor '9.7', entre comillas. ¿Que dice eso?Nivel 2 · Predecir la salida
4. ¿Que es la Consola de depuracion?Nivel 2 · Predecir la salida
5. Un bucle recorre 40 filas y solo una tiene el problema. ¿Como frenas ahi sin apretar F5 cuarenta veces?Nivel 3 · Elegir entre dos casi correctas
6. Tu script anda desde la terminal, pero con F5 tira FileNotFoundError con el CSV. ¿Que paso?Nivel 3 · Elegir entre dos casi correctas
7. Este programa muere con TypeError: unsupported operand type(s) for +: 'float' and 'str'. Pusiste un breakpoint en la ultima linea y frenaste ahi. ¿Cual es la primera expresion que conviene escribir en la consola?
notas = pd.read_csv("estudiantes_notas.csv")
columnas = ["nota_1", "nota_2", "nota_3"]

notas["promedio"] = notas[columnas].mean(axis=1)
Nivel 4 · Encontrar el defecto