# Errores y depuración

URL canónica: https://missyera.com/guias/python-desde-cero/errores-y-depuracion/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> 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.

Capítulo 7 de 17 del libro [Python desde cero](https://missyera.com/guias/python-desde-cero.md).

## 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

| Error | Qué significa | Cuándo aparece con datos |
|---|---|---|
| `TypeError` | Le pediste algo imposible a ese tipo | Sumar un texto con un número |
| `ValueError` | El tipo está bien, el valor no | Convertir `'n/a'` a número |
| `KeyError` | Esa clave no existe | Una columna que no vino en el archivo |
| `IndexError` | Esa posición no existe | Una fila con menos campos de los esperados |
| `FileNotFoundError` | No está el archivo | La ruta, siempre la ruta |
| `ZeroDivisionError` | División entre cero | Un 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.](https://missyera.com/static/diagramas/webp/py-06-errores.webp)

*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)
```

```text
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')
```

```text
'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)
```

```text
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}')
```

```text
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')
```

```text
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))
```

```text
<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(',', '.')))
```

```text
'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}')
```

```text
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__)
```

```text
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}')
```

```text
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)
```

```text
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')
```

```text
[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)
```

```text
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)
```

```text
{'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]))
```

```text
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))
```

```text
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)
```

```text
{'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}')
```

```text
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__))
```

```text
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))
```

```text
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())
```

```text
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](https://missyera.com/guias/machine-learning-desde-cero/) 🤖

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.

## Seguir leyendo

- Anterior: [Funciones](https://missyera.com/guias/python-desde-cero/funciones.md)
- Siguiente: [Módulos, paquetes y entornos virtuales](https://missyera.com/guias/python-desde-cero/modulos-y-entornos.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/python-desde-cero/errores-y-depuracion/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
