Hola! Vamos a hacernos amigas de los errores
Te lo dije en el capítulo 1 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.
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.
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, noexcepta 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)
- ✅
elsecorre solo si no hubo error. - 🔒
finallycorre siempre, con error o sin él, incluso si hay unreturnen 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 1.
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 😄
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.
Lo que te llevas
- 🎯 Atrapa el error concreto, nunca
excepta secas. - 📢 Nunca
passsin dejar rastro: cuenta, guarda o imprime. - 🛑 Un
raisetuyo, a tiempo, evita un reporte absurdo. - 🔍
repr()ytype()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é.
En el capítulo 7 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! 🌸