Capítulo 7 de 17 14 secciones 20 min

Compartir

Errores y depuración

Los errores que de verdad salen con datos, cómo atraparlos sin taparlos, y cómo encontrar el problema sin volverte loca.

Los errores se atrapan con try y except. La regla que uso siempre es atrapar solo el error concreto que espero, nunca un except vacío que se traga todo. Ese except vacío convierte un fallo ruidoso, que alguien arregla esa misma tarde, en datos silenciosamente equivocados que nadie descubre hasta el cierre de mes.

Hola! Vamos a hacernos amigas de los errores

Te lo dije en el capítulo 2 y lo repito porque importa: los errores no son un castigo, son información 💜

Un error que revienta es el mejor de los casos. El peligroso es el que no revienta y te deja un número mal, que es del que va buena parte de este capítulo.

Y para en seco un segundo antes de seguir: ¿alguna vez entregaste un número sin comprobar de dónde salía? Todas lo hicimos alguna vez. Este capítulo va de que no vuelva a pasar sin que te enteres 💜

Los seis errores que de verdad vas a ver

ErrorQué significaCuándo aparece con datos
TypeErrorLe pediste algo imposible a ese tipoSumar un texto con un número
ValueErrorEl tipo está bien, el valor noConvertir 'n/a' a número
KeyErrorEsa clave no existeUna columna que no vino en el archivo
IndexErrorEsa posición no existeUna fila con menos campos de los esperados
FileNotFoundErrorNo está el archivoLa ruta, siempre la ruta
ZeroDivisionErrorDivisión entre ceroUn promedio de una lista vacía

Vamos a verlos de verdad, que es como se aprenden:

int('n/a')
ValueError: invalid literal for int() with base 10: 'n/a'
venta = {'ciudad': 'Lima'}
venta['descuento']
KeyError: 'descuento'
sum([]) / len([])
ZeroDivisionError: division by zero

Fíjate en la diferencia entre los dos primeros. int('12') funciona, o sea que int() sí acepta textos: el tipo estaba bien y lo que estaba mal era el valor. Eso es un ValueError.

En cambio '12' + 5 es un TypeError, porque la operación misma no tiene sentido entre esos dos tipos.

Árbol sobre qué hacer cuando algo falla: si no sabes qué hacer con el error, dejarlo reventar. Si sabes, atrapar el error concreto y contarlo, registrarlo o avisar. Y si lo tapas con un except vacío, el resultado es un total al que le falta dinero.
Las tres salidas son código que corre. Solo una te deja enterarte, y la de abajo a la derecha es la que convierte un fallo ruidoso en un número mal que nadie va a revisar.

try y except

valores = ['1250.50', '480,37', 'n/a', '95.5', '']

limpios = []
descartados = []

for v in valores:
    try:
        limpios.append(float(v))
    except ValueError:
        descartados.append(v)

print('limpios     :', limpios)
print('descartados :', descartados)
limpios     : [1250.5, 95.5]
descartados : ['480,37', 'n/a', '']

Eso ya es código útil de verdad: procesa lo que puede y guarda lo que no pudo para mirarlo después.

Y mira qué se ve ahí: '480,37' no era basura, era un número con coma decimal. Si lo hubiera botado sin mirar, habría perdido una venta buena. Por eso los descartes se guardan, no se ignoran 🔍

for v in descartados:
    print(repr(v), '->', float(v.replace(',', '.')) if v.replace(',', '').replace('.', '').isdigit() else 'sin arreglo')
'480,37' -> 480.37
'n/a' -> sin arreglo
'' -> sin arreglo

El anti-patrón que más daño hace

Esto lo vas a ver en internet y en código de gente con años de experiencia. No lo copies.

valores = ['1250.50', 'n/a', '95.5']
total = 0

for v in valores:
    try:
        total = total + float(v)
    except:
        pass

print(total)
1346.0

Funciona. Ese es justo el problema 😖

Un except pelado se traga todo: el valor inválido que esperabas, pero también un error de tipeo tuyo, un problema de memoria, una interrupción con Ctrl+C. Y pass significa "no hagas nada al respecto".

Entonces un día tu proceso devuelve un total que es la mitad del real y no hay ni una línea en ningún lado que diga qué pasó. Lo he visto costar semanas de confianza en un equipo de datos.

Las dos reglas, y son duras:

  • 🎯 Atrapa el error concreto que esperas: except ValueError, no except a secas.
  • 📢 Haz algo con él. Guárdalo, cuéntalo, imprímelo. Nunca pass.
valores = ['1250.50', 'n/a', '95.5']
total = 0
problemas = []

for v in valores:
    try:
        total = total + float(v)
    except ValueError as e:
        problemas.append((v, str(e)))

print(f'Total: {total}')
print(f'Problemas: {len(problemas)}')
for valor, mensaje in problemas:
    print(f'  {repr(valor)}: {mensaje}')
Total: 1346.0
Problemas: 1
  'n/a': could not convert string to float: 'n/a'

El mismo total, pero ahora sabes que hubo un problema y cuál fue. Esa diferencia es la que separa un script de un proceso en el que se puede confiar 🌸

else y finally

def leer_monto(texto):
    try:
        valor = float(texto)
    except ValueError:
        print(f'  {repr(texto)} no es un numero')
        return None
    else:
        print(f'  {repr(texto)} convertido bien')
        return valor
    finally:
        print('  (esto sale siempre)')

print('Caso bueno:')
leer_monto('480.37')
print('Caso malo:')
leer_monto('n/a')
Caso bueno:
  '480.37' convertido bien
  (esto sale siempre)
Caso malo:
  'n/a' no es un numero
  (esto sale siempre)
  • else corre solo si no hubo error.
  • 🔒 finally corre siempre, con error o sin él, incluso si hay un return en medio. Es donde se cierran archivos y conexiones.

Levantar tus propios errores

A veces el error no lo produce Python:

  • Lo produces tú
  • Porque los datos no tienen sentido
  • Seguir sería peor
def calcula_descuento(monto, porcentaje):
    if not 0 <= porcentaje <= 1:
        raise ValueError(
            f'El porcentaje debe ir entre 0 y 1, y llegó {porcentaje}. '
            f'Si viene como 15 en vez de 0.15, divídelo entre 100.')
    return round(monto * porcentaje, 2)

print(calcula_descuento(1000, 0.15))
calcula_descuento(1000, 15)
ValueError: El porcentaje debe ir entre 0 y 1, y llegó 15. Si viene como 15 en vez de 0.15, divídelo entre 100.

Sin ese raise, el descuento de 15 habría dado S/15.000 sobre una venta de S/1.000. Habría corrido perfecto y el reporte habría salido absurdo.

Un buen mensaje de error dice tres cosas: qué se esperaba, qué llegó, y qué hacer al respecto. Ese de arriba dice las tres 💜

Cómo depurar sin volverte loca

Cuando algo no funciona y no entiendes por qué, este es el orden que a mí me funciona.

1. Imprime el tipo antes que el valor.

dato = '1250.50'

print(type(dato), repr(dato))
<class 'str'> '1250.50'

El repr es clave: te muestra las comillas y los espacios invisibles. La mitad de los misterios se resuelven aquí.

2. Parte el problema en pedazos. Si una línea larga falla, pártela y mira cada trozo.

fila = '  C0045 ; Arequipa ; 480,37  '

paso1 = fila.strip()
paso2 = paso1.split(';')
paso3 = [p.strip() for p in paso2]

print(repr(paso1))
print(paso2)
print(paso3)
print(float(paso3[2].replace(',', '.')))
'C0045 ; Arequipa ; 480,37'
['C0045 ', ' Arequipa ', ' 480,37']
['C0045', 'Arequipa', '480,37']
480.37

Ahí se ve clarito que después de partir por punto y coma quedaban espacios, y que por eso hacía falta el segundo strip.

3. Reduce el caso. Si falla con tres mil filas, prueba con la primera. Casi siempre falla también, y depurar una fila es mil veces más rápido que depurar tres mil.

4. Lee el traceback de abajo hacia arriba y busca la última línea que menciona tu código. Eso ya lo tienes del capítulo 2.

5. Cuando nada funcione, explícaselo en voz alta a alguien. Te vas a dar cuenta sola a mitad de la explicación. A mí me pasa siempre, y no tengo idea de por qué funciona tan bien 😄

La traza es un mapa, no un castigo

Arriba te dije "lee el traceback de abajo hacia arriba". Ahora te lo enseño con uno de verdad, porque cuando hay funciones llamando a funciones el mapa tiene varias paradas 🗺️

import traceback


def limpia_monto(texto):
    return float(texto.replace(',', '.'))


def total_de(filas):
    return sum(limpia_monto(f['monto']) for f in filas)


filas = [{'monto': '480.37'}, {'monto': '524,90'}, {'monto': 'n/a'}]

try:
    total_de(filas)
except ValueError as e:
    print('el error  :', type(e).__name__)
    print('el mensaje:', e)
    print()
    print('por donde paso, de fuera hacia dentro:')
    for marco in traceback.extract_tb(e.__traceback__):
        print(f'  linea {marco.lineno:>3}  dentro de  {marco.name}')
el error  : ValueError
el mensaje: could not convert string to float: 'n/a'

por donde paso, de fuera hacia dentro:
  linea  15  dentro de  <module>
  linea   9  dentro de  total_de
  linea   9  dentro de  <genexpr>
  linea   5  dentro de  limpia_monto

Eso es exactamente lo que te sale en rojo cuando algo revienta, solo que aquí lo pedí ordenadito con traceback 🔍

Léelo así:

  • 🔻 De arriba abajo es de fuera adentro. Arriba está la línea que tú escribiste, abajo la línea que finalmente falló.
  • 🎯 La última parada es dónde reventó. Ahí está el bug si el código es tuyo.
  • 👀 La primera parada tuya, contando desde abajo, es dónde mirar. Cuando la última parada está dentro de pandas o de numpy, el problema casi nunca es de pandas: es lo que tú le pasaste.

Y ese <genexpr> que aparece en medio no es un error tuyo: es la expresión generadora de dentro del sum(). Verás también <listcomp> por las comprensiones de lista y <module> por el nivel de arriba del todo, el del cuaderno.

El error que no dice qué fila

Mira el mensaje de arriba otra vez: could not convert string to float: 'n/a'. Te dice el valor. No te dice cuál de las tres mil filas es, y ese es el dato que necesitas para arreglarlo 😖

La solución es atraparlo, añadirle lo que tú sí sabes, y volver a lanzarlo:

def monto_de_la_fila(numero, texto):
    try:
        return float(texto.replace(',', '.'))
    except ValueError as e:
        raise ValueError(f'fila {numero}: el monto dice {texto!r}') from e


try:
    monto_de_la_fila(1847, 'n/a')
except ValueError as e:
    print('lo que ves tu   :', e)
    print('lo que paso antes:', e.__cause__)
lo que ves tu   : fila 1847: el monto dice 'n/a'
lo que paso antes: could not convert string to float: 'n/a'

Ahí tienes las dos mitades 💡

El mensaje de arriba es el útil, el que dice fila 1847. Y el de abajo, el original, no se pierde: queda enganchado en __cause__ gracias al from e.

Eso es lo que en la pantalla te sale como The above exception was the direct cause of the following exception, con las dos trazas una encima de otra. Que parece que fallaron dos cosas y no: es la misma, contada dos veces, la de fuera con más información.

Si te olvidas del from e, Python engancha el original igual pero con otra etiqueta: During handling of the above exception, another exception occurred, que suena a "y encima falló otra cosa". Es la diferencia entre "esto lo causó aquello" y "esto pasó mientras manejaba aquello", y la primera es la verdad. Un carácter, dos lecturas distintas.

Un procesador de archivo que aguanta datos feos

filas = [
    'C0045;Arequipa;480.37',
    'C0325;Lima;524,90',
    'C0260;Cusco;n/a',
    'C0111;Piura',
    'C0777;Trujillo;1200.00',
]

buenas = []
rechazadas = []

for numero, fila in enumerate(filas, start=1):
    partes = fila.split(';')

    if len(partes) != 3:
        rechazadas.append((numero, fila, 'no tiene 3 campos'))
        continue

    codigo, ciudad, monto_texto = partes
    try:
        monto = float(monto_texto.replace(',', '.'))
    except ValueError:
        rechazadas.append((numero, fila, f'monto ilegible: {monto_texto}'))
        continue

    buenas.append({'codigo': codigo, 'ciudad': ciudad, 'monto': monto})

print(f'{len(buenas)} filas buenas, {len(rechazadas)} rechazadas')
print(f'Total: S/{sum(b["monto"] for b in buenas):,.2f}')
print()
for numero, fila, motivo in rechazadas:
    print(f'  fila {numero}: {motivo}')
3 filas buenas, 2 rechazadas
Total: S/2,205.27

  fila 3: monto ilegible: n/a
  fila 4: no tiene 3 campos

Eso es lo que quiero que te lleves de este capítulo. No es que no falle: es que cuando falla, te dice qué fila y por qué, y sigue con las demás.

Ese informe de rechazos es lo que le mandas a quien te pasó el archivo. Y es la diferencia entre "tu archivo tiene errores" y "las filas 3 y 4 tienen esto, ¿me las puedes corregir?" 🌸

Ejercicios

1. Reconoce el error

Provoca un ValueError y un TypeError, y explica en qué se diferencian.

try:
    int('sin dato')          # la celda que alguien dejo escrita a mano
except ValueError as e:
    print('ValueError:', e)

try:
    '12' + 5                 # unidades como texto mas un numero
except TypeError as e:
    print('TypeError:', e)
ValueError: invalid literal for int() with base 10: 'sin dato'
TypeError: can only concatenate str (not "int") to str

En el primero int() sí acepta textos, este texto en particular no servía. En el segundo la operación misma no existe entre esos tipos.

2. Convertir sin morir

Convierte una lista de textos a número, saltándote los que no se puedan, y cuenta cuántos se salvaron.

montos = ['100', '250.5', 'n/a', '', '1200']    # como llegan del sistema

numeros = []
for m in montos:
    try:
        numeros.append(float(m))
    except ValueError:
        pass

print(numeros, f'{len(numeros)} de {len(montos)} montos legibles')
[100.0, 250.5, 1200.0] 3 de 5 montos legibles

Aquí el pass está bien porque la línea de abajo cuenta cuántos se perdieron. El anti-patrón es el pass que no deja rastro de nada.

3. El except pelado, y por qué no

Escribe un except vacío que tape un error de tipeo tuyo, para ver lo fácil que es.

montos = ['10', '20']
total = 0

for m in montos:
    try:
        total = total + flotar(m)     # esta funcion no existe
    except:
        pass

print('Total de la caja del día:', total)
Total de la caja del día: 0

Cero, sin una sola queja. El except pelado se tragó un NameError que no tenía nada que ver con los datos. Con except ValueError habría reventado y te habrías enterado.

4. Clave que puede no venir

Recorre una lista de ventas donde algunas no traen canal y arma el conteo por canal sin que reviente.

ventas = [
    {'ciudad': 'Lima', 'canal': 'Web'},
    {'ciudad': 'Cusco'},
    {'ciudad': 'Lima', 'canal': 'Web'},
]

conteo = {}
for v in ventas:
    canal = v.get('canal', 'Sin canal')
    conteo[canal] = conteo.get(canal, 0) + 1

print(conteo)
{'Web': 2, 'Sin canal': 1}

Aquí ni hizo falta el try: .get() con valor por defecto lo resuelve más limpio. Cuando exista una forma de prevenir el error, es mejor que atraparlo.

5. Tu propio raise

Escribe una función que rechace unidades negativas con un mensaje que explique qué hacer.

def registra_venta(unidades):
    if unidades < 0:
        raise ValueError(
            f'Las unidades no pueden ser negativas y llegaron {unidades}. '
            f'Si es una devolución, va en el campo de notas de crédito.')
    return f'{unidades} unidades registradas'

print(registra_venta(10))
registra_venta(-3)
ValueError: Las unidades no pueden ser negativas y llegaron -3. Si es una devolución, va en el campo de notas de crédito.
6. La división que a veces es cero

Calcula el promedio de una lista que puede venir vacía, de dos formas: con try y con if.

def ticket_try(montos):
    try:
        return sum(montos) / len(montos)
    except ZeroDivisionError:
        return 0

def ticket_if(montos):
    if not montos:
        return 0
    return sum(montos) / len(montos)

# El filtro de una ciudad que ese dia no vendio nada
print(ticket_try([]), ticket_if([]))
print(ticket_try([10, 20]), ticket_if([10, 20]))
0 0
15.0 15.0

Las dos valen. La del if se lee mejor porque el caso vacío no es excepcional, es normal.

7. Depurar con repr

Tienes dos códigos de cliente que parecen iguales y no lo son. Encuentra la diferencia.

a = 'C0045'
b = 'C0045 '

print(a == b)
print(repr(a), repr(b))
print(len(a), len(b))
False
'C0045' 'C0045 '
5 6

Un espacio al final. Con print normal es invisible y te puede costar una tarde de "pero si son iguales" 🙃

8. Contar los rechazos por motivo

Procesa una lista de filas y arma un conteo de rechazos agrupado por motivo.

montos = ['100', 'n/a', '', '250', 'abc']    # cinco filas del archivo

motivos = {}
for f in montos:
    if f == '':
        motivos['vacío'] = motivos.get('vacío', 0) + 1
        continue
    try:
        float(f)
    except ValueError:
        motivos['no numérico'] = motivos.get('no numérico', 0) + 1

print(motivos)
{'no numérico': 2, 'vacío': 1}

Este conteo por motivo es lo primero que te van a pedir cuando digas "el archivo tiene problemas". Sin él, la conversación no avanza.

9. La traza de una columna que no vino

Provoca un KeyError a dos niveles de profundidad y lee el mapa.

def ciudad_de(venta):
    return venta['ciudad'].strip().title()


def ciudades_de(ventas):
    return [ciudad_de(v) for v in ventas]


ventas = [{'ciudad': ' lima '}, {'ciudad': 'AREQUIPA'}, {'region': 'Cusco'}]

try:
    ciudades_de(ventas)
except KeyError as e:
    print('falto la clave:', e)
    for marco in traceback.extract_tb(e.__traceback__):
        print(f'  linea {marco.lineno:>3}  dentro de  {marco.name}')
falto la clave: 'ciudad'
  linea  12  dentro de  <module>
  linea   6  dentro de  ciudades_de
  linea   6  dentro de  <listcomp>
  linea   2  dentro de  ciudad_de

Fíjate en lo que la traza te da y en lo que no 👀

Te dice que reventó dentro de ciudad_de, y eso está perfecto. No te dice cuál de las tres ventas venía sin ciudad, que es justamente lo que necesitas para llamar a quien te pasó el archivo.

Por eso existe el ejercicio siguiente.

10. Con from y sin from

Lanza el mismo error de las dos maneras y mira qué guarda Python en cada caso.

def con_from(texto):
    try:
        return float(texto)
    except ValueError as e:
        raise ValueError(f'monto ilegible: {texto!r}') from e


def sin_from(texto):
    try:
        return float(texto)
    except ValueError:
        raise ValueError(f'monto ilegible: {texto!r}')


for funcion in (con_from, sin_from):
    try:
        funcion('n/a')
    except ValueError as e:
        print(funcion.__name__)
        print('   causa declarada:', repr(e.__cause__))
        print('   causa de fondo :', repr(e.__context__))
con_from
   causa declarada: ValueError("could not convert string to float: 'n/a'")
   causa de fondo : ValueError("could not convert string to float: 'n/a'")
sin_from
   causa declarada: None
   causa de fondo : ValueError("could not convert string to float: 'n/a'")

Los dos conservan el error original, así que no pierdes información en ninguno de los dos casos 🙂

La diferencia es que __cause__ solo se llena cuando tú lo declaras. Es tu forma de decir "sé perfectamente por qué pasó esto y lo estoy traduciendo", en vez de dejar que parezca un accidente encima de otro.

11. Un error con tu nombre y tus datos

Crea tu propio tipo de error, que además guarde el número de fila para poder usarlo después.

class MontoIlegible(ValueError):
    def __init__(self, fila, texto):
        self.fila = fila
        self.texto = texto
        super().__init__(f'fila {fila}: no puedo leer {texto!r} como monto')


filas = ['480.37', 'n/a', '1200,00', '']
limpios, fallos = [], []

for numero, texto in enumerate(filas, start=1):
    try:
        try:
            limpios.append(float(texto.replace(',', '.')))
        except ValueError as e:
            raise MontoIlegible(numero, texto) from e
    except MontoIlegible as e:
        fallos.append(e)

print('leidos:', limpios)
for e in fallos:
    print('  ', e, '| fila guardada aparte:', e.fila)
print('y sigue siendo un ValueError:', isinstance(fallos[0], ValueError))
leidos: [480.37, 1200.0]
   fila 2: no puedo leer 'n/a' como monto | fila guardada aparte: 2
   fila 4: no puedo leer '' como monto | fila guardada aparte: 4
y sigue siendo un ValueError: True

Dos cosas que hacen que esto valga la pena 🌸

Guarda datos, no solo texto. Ese e.fila es un número de verdad, así que puedes contarlos, ordenarlos o escribirlos en un CSV de rechazos. Un mensaje de error no se puede sumar.

Hereda de ValueError, no de Exception. Por eso el último print sale True: quien tenga un except ValueError escrito de antes lo sigue atrapando, y no le rompes el código a nadie por añadir tu clase.

Y sí, el try dentro del try se ve raro. El de dentro traduce y el de fuera recoge. En un programa de verdad el de fuera estaría en otro sitio, seguramente en quien llama a la función.

12. Dejar rastro con logging

Sustituye los print de depuración por un registro con niveles.

import io
import logging

registro = io.StringIO()
log = logging.getLogger('ventas')
log.handlers.clear()
log.setLevel(logging.DEBUG)
manejador = logging.StreamHandler(registro)
manejador.setFormatter(logging.Formatter('%(levelname)-8s %(message)s'))
log.addHandler(manejador)
log.propagate = False

for numero, texto in enumerate(filas, start=1):
    try:
        valor = float(texto.replace(',', '.'))
    except ValueError:
        log.warning('fila %s descartada: %r', numero, texto)
    else:
        log.debug('fila %s leida: %s', numero, valor)

log.info('%s de %s filas leidas', len(limpios), len(filas))
print(registro.getvalue().rstrip())
DEBUG    fila 1 leida: 480.37
WARNING  fila 2 descartada: 'n/a'
DEBUG    fila 3 leida: 1200.0
WARNING  fila 4 descartada: ''
INFO     2 de 4 filas leidas

Un print siempre imprime. Un log imprime si el nivel lo permite, y esa es toda la gracia 🎚️

Aquí lo puse en DEBUG, que es el más hablador, y por eso salen las cinco líneas. Cámbialo a logging.WARNING y desaparecen los debug: quedan solo los descartes. Sin tocar una sola línea del bucle.

Eso es lo que quieres cuando el script corre solo todas las noches: hablador mientras lo pruebas, callado cuando ya funciona, y con las advertencias siempre visibles 🌙

Aquí escribí a memoria con io.StringIO() para que la salida se vea en el libro. En tu máquina cambias esa línea por logging.FileHandler('proceso.log') y tienes el registro en un archivo que puedes leer al día siguiente.

Y el %s en vez de f-string no es capricho: con esa forma, si el nivel apaga el mensaje, Python ni siquiera se molesta en armar el texto.

Cuando atrapar el error es peor que no atraparlo

Todo este capítulo va de no dejar que las cosas revienten en silencio. Así que toca mirar el otro lado: el try escrito con las mejores intenciones que termina restando dinero de un total 🕳️

La trampa

Atrapas el error concreto, como manda el capítulo, y devuelves None para que el programa no se caiga. Es lo prudente.

def a_numero(texto):
    try:
        return float(texto)
    except ValueError:
        return None

montos = [a_numero(x) for x in columna]
print(sum(m for m in montos if m))
Qué está mal

El programa no se cae, y eso es justo el problema 🕳️ Cada monto que no se pudo convertir desaparece de la suma sin dejar rastro. El total sale, se ve razonable, y le falta dinero.

Es el mismo desastre que errors='coerce' en pandas, pero escrito a mano: convertiste un error ruidoso en un número silencioso y equivocado. Y un total equivocado es mucho peor que un programa caído, porque el programa caído lo arregla alguien esa misma tarde.

Atrapar un error solo vale la pena si haces algo con él: contarlo, registrarlo, avisar. La regla que uso es que si no sabes qué hacer con el error, no lo atrapes. Que reviente y te obligue a decidir.

Comprueba que lo tienes

Envuelves cien líneas en un try con un except pelado y ya no falla nada. ¿Está bien?

  • No: ahora los errores pasan igual y nadie se entera
  • Sí, mientras el programa no se caiga
  • Sí, si además imprimes un mensaje
  • No, porque try es lento

Lo que te llevas

  • 🎯 Atrapa el error concreto, nunca except a secas.
  • 📢 Nunca pass sin dejar rastro: cuenta, guarda o imprime.
  • 🛑 Un raise tuyo, a tiempo, evita un reporte absurdo.
  • 🔍 repr() y type() resuelven la mitad de los misterios.
  • 📋 Un buen proceso no es el que no falla, es el que dice qué fila falló y por qué.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Si no sabes qué hacer con un error, no lo atrapes. Que reviente y te obligue a decidir.

Y este capítulo importa más de lo que parece por lo que viene después: un total al que le faltan filas en silencio es exactamente lo que termina entrenando un modelo torcido, y de eso va el libro de machine learning 🤖

En el capítulo 8 salimos del cuaderno: módulos, import, y los entornos virtuales, que son la respuesta a "en mi máquina funcionaba".

Que tengas lindo día! 🌸

Preguntas frecuentes

¿Cómo funciona try except en Python?

Lo que va en try se intenta; si revienta, se ejecuta el except en vez de cortar el programa. Conviene atrapar el error concreto y no todos, porque un except pelado se traga también los errores tuyos.

¿Qué es un traceback?

La lista de por dónde pasó el programa antes de reventar. Se lee de abajo hacia arriba: la última línea dice qué pasó y la penúltima dónde.

¿Qué significa TypeError en Python?

Que le pediste a un tipo de dato algo que no sabe hacer, como sumar un texto con un número. Suele venir de un CSV donde una columna vino como texto.

¿Qué significa KeyError?

Que pediste una clave que no está en el diccionario. Casi siempre es un nombre de columna con un espacio de más o en otra caja.

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?