lección 7

Errores y try/except: cuando las cosas fallan

Aprende a leer errores, depurar bugs y manejar fallos con try/except — la habilidad que separa a un junior de un profesional.

60 min

Aquí va una verdad incómoda de la ingeniería de datos: las cosas SIEMPRE fallan. El CSV viene con una fila corrupta. La API devuelve un error 500 a las 3 de la madrugada. El archivo que esperabas no existe porque alguien lo movió. Un campo que debería ser numérico tiene el texto "N/A". Si tu script no sabe manejar estas situaciones, muere silenciosamente o peor — produce datos incorrectos sin avisar.

try/except es tu red de seguridad. Le dice a Python: "intenta ejecutar este código, y si algo sale mal, no explotes — haz esto otro en su lugar". Es la diferencia entre un script de principiante que funciona solo con datos perfectos y un pipeline profesional que sobrevive en producción.

### Anatomía de un error de Python

Antes de aprender a MANEJAR errores, necesitas saber LEERLOS. Cuando Python encuentra un problema, no simplemente dice "error" y se calla. Te imprime un traceback — un informe detallado de qué pasó, dónde pasó y por qué. El problema es que la mayoría de principiantes ven ese bloque de texto rojo y entran en pánico. No deberías. Un traceback es un MAPA que te lleva directamente al problema.

La regla de oro: lee el traceback DE ABAJO ARRIBA. La última línea es la más importante — te dice QUÉ falló. Las líneas de arriba te dicen DÓNDE (archivo, función, línea exacta). Un traceback tiene tres partes: la cadena de llamadas (cómo llegó Python hasta ahí), la línea exacta que falló (el código que causó el problema), y el tipo de error con su mensaje (qué salió mal y por qué).

Un traceback se lee de abajo arriba. La última línea es la más importante.

### Los 5 errores que más vas a ver

Después de 5 lecciones escribiendo Python, seguro que ya te has topado con alguno de estos. En ingeniería de datos, estos 5 tipos de error representan el 90% de los problemas cotidianos — cuando procesas CSVs, conviertes tipos o accedes a posiciones de una lista. Reconocerlos al instante te ahorra horas de frustración.

1# 1. SyntaxError — Escribiste mal el código (Python no puede ni leerlo)
2# Causa: falta un ":", un paréntesis, indentación incorrecta
3if True
4 print("falta los dos puntos") # SyntaxError: expected ':'
5
6# 2. NameError — Usas una variable que no existe
7# Causa: typo en el nombre, olvidaste definirla, scope incorrecto
8print(total_ventas) # NameError: name 'total_ventas' is not defined
9
10# 3. TypeError — Mezclas tipos incompatibles
11# Causa: sumar string + int, pasar argumento equivocado
12edad = "25"
13anio_nacimiento = 2024 - edad # TypeError: unsupported operand type(s)
14
15# 4. IndexError — Accedes a una posición que no existe
16# Causa: lista más corta de lo esperado, off-by-one
17filas = ["a", "b", "c"]
18print(filas[5]) # IndexError: list index out of range
19
20# 5. ValueError — El valor no es convertible al tipo esperado
21# Causa: datos sucios en CSVs, campos vacíos, texto donde esperabas números
22precio = float("N/A") # ValueError: could not convert string to float

Estos 5 errores cubren el 90% de lo que verás en tus primeros meses trabajando con datos.

### La técnica del print() detective

Los errores que hemos visto producen un crash — Python se detiene y te dice qué pasó. Pero hay otra categoría de bugs más peligrosa: los errores lógicos. Tu script no explota, termina sin errores... pero el resultado está MAL. Calculaste un promedio incorrecto, filtraste de más, o el pipeline produjo un CSV vacío. Estos bugs no tienen traceback — tienes que cazarlos tú.

La técnica más simple y efectiva para depurar errores lógicos es el "print() detective": colocar prints estratégicos para inspeccionar el estado de tus variables en puntos clave del flujo. No es elegante, pero funciona. Incluso ingenieros senior con 15 años de experiencia usan print() para entender qué está pasando dentro de un pipeline.

1# Bug: el total de ventas da 0, pero el CSV tiene datos
2ventas = []
3with open("ventas.csv") as f:
4 for linea in f:
5 campos = linea.strip().split(",")
6 # print() detective: ver que hay en campos
7 print(f"DEBUG campos: {campos}") # <-- inspeccionar
8
9 precio = campos[2]
10 # print() detective: ver el tipo y valor
11 print(f"DEBUG precio: {repr(precio)}, tipo: {type(precio)}") # <-- inspeccionar
12
13 ventas.append(precio) # BUG: es string, no float!
14
15# El print revela: precio es "45.99" (string), no 45.99 (float)
16# Solucion: ventas.append(float(precio))
17
18total = sum(ventas) # TypeError si son strings, o 0 si la lista esta vacia
19print(f"DEBUG total: {total}, len(ventas): {len(ventas)}") # <-- verificar resultado

print() con repr() es tu mejor aliado: muestra el valor EXACTO (incluyendo comillas, espacios ocultos, tipo).

Consejo de senior: cuando te sale un error que no entiendes, copia la ÚLTIMA línea del traceback (ej: "ValueError: could not convert string to float: 'N/A'") y pégala tal cual en Google. El primer resultado de Stack Overflow casi siempre tiene la solución. No es trampa — es literalmente lo que hacen los profesionales todos los días. Saber googlear errores es una habilidad técnica legítima.

Ahora que sabes LEER errores y depurar bugs lógicos, vamos a aprender a MANEJARLOS con try/except — para que tu pipeline no muera ante el primer dato sucio y puedas controlar qué hace tu programa cuando las cosas inevitablemente fallan.

### El bloque try/except básico

La analogía: imagina que eres un cocinero. Sabes que a veces el huevo viene roto. En vez de tirar toda la tortilla, separas el huevo roto y sigues con los demás. try/except es exactamente eso — aislar el fallo para que no contamine todo el proceso.

1# SIN try/except — explota con datos sucios
2precios = ["12.99", "45.00", "N/A", "89.50", "", "120.00"]
3
4# Esto FALLARÁ en "N/A" y "" → ValueError
5# totales = [float(p) for p in precios]
6
7# CON try/except — maneja datos sucios
8totales = []
9errores = []
10
11for i, precio in enumerate(precios):
12 try:
13 valor = float(precio)
14 totales.append(valor)
15 except ValueError:
16 errores.append(f"Fila {i}: '{precio}' no es un número válido")
17
18print(f"Procesados: {len(totales)} valores")
19print(f"Errores: {len(errores)} filas con problemas")
20for error in errores:
21 print(f" ⚠️ {error}")

try/except convierte un crash en información útil para depurar.

### Tipos de excepciones comunes en datos

Python tiene muchos tipos de excepciones. Cada una indica un problema diferente. En datos, te encontrarás con estas constantemente:

  • ValueError — Un valor no se puede convertir: float("N/A"), int("abc")
  • KeyError — Buscas una clave que no existe en un diccionario: registro["campo_inexistente"]
  • FileNotFoundError — El archivo que intentas leer no existe
  • TypeError — Operas con tipos incompatibles: "texto" + 5
  • ZeroDivisionError — Divides entre cero (frecuente al calcular promedios con datos vacíos)
  • IndexError — Accedes a una posición que no existe en una lista
  • json.JSONDecodeError — El JSON que intentas parsear está malformado
  • UnicodeDecodeError — El archivo tiene una codificación diferente a la esperada
1# Capturar excepciones específicas (RECOMENDADO)
2try:
3 with open("datos.csv", "r", encoding="utf-8") as f:
4 contenido = f.read()
5except FileNotFoundError:
6 print("El archivo no existe. ¿Se movió o se renombró?")
7except PermissionError:
8 print("No tienes permisos para leer este archivo.")
9except UnicodeDecodeError:
10 print("El archivo no está en UTF-8. Prueba con encoding='latin-1'.")
11
12# Capturar CUALQUIER excepción (usar con cuidado)
13try:
14 resultado = operacion_arriesgada()
15except Exception as e:
16 print(f"Error inesperado: {type(e).__name__}: {e}")

Sé específico con las excepciones. Captura lo que esperas, no "todo".

NUNCA uses "except:" sin especificar el tipo de excepción (bare except). Captura TODO incluyendo errores de programación que DEBERÍAN hacer crash para que los veas. Como mínimo usa "except Exception as e:" y loguea el error. Un error silenciado es un bug invisible.

### El patrón try/except/else/finally

1import json
2
3def cargar_config(ruta):
4 """Carga configuración JSON con manejo robusto de errores."""
5 try:
6 with open(ruta, "r", encoding="utf-8") as f:
7 config = json.load(f)
8 except FileNotFoundError:
9 print(f"⚠️ {ruta} no existe. Usando configuración por defecto.")
10 config = {"umbral": 100, "formato_salida": "json"}
11 except json.JSONDecodeError as e:
12 print(f"❌ {ruta} tiene JSON inválido: {e}")
13 config = {"umbral": 100, "formato_salida": "json"}
14 else:
15 # Se ejecuta SOLO si no hubo excepción
16 print(f"✓ Configuración cargada desde {ruta}")
17 finally:
18 # Se ejecuta SIEMPRE (haya o no error)
19 print(f" Config activa: {config}")
20
21 return config
22
23# Probar con archivo existente y no existente
24config = cargar_config("config.json")
25config2 = cargar_config("no_existe.json")

else se ejecuta si NO hay error. finally se ejecuta SIEMPRE.

### Patrón profesional: procesar con tolerancia a fallos

En producción, el patrón más robusto es: procesar todos los registros, acumular errores sin parar, y al final reportar qué funcionó y qué falló. Así no pierdes los 99 registros buenos por culpa de 1 malo.

1def procesar_ventas(registros):
2 """Procesa ventas con tolerancia a fallos."""
3 exitosos = []
4 fallidos = []
5
6 for i, reg in enumerate(registros):
7 try:
8 # Validar campos requeridos
9 if "producto" not in reg or "precio" not in reg:
10 raise ValueError("Faltan campos requeridos")
11
12 # Convertir y calcular
13 precio = float(reg["precio"])
14 cantidad = int(reg.get("cantidad", 1))
15
16 if precio < 0:
17 raise ValueError(f"Precio negativo: {precio}")
18
19 reg["total"] = precio * cantidad
20 exitosos.append(reg)
21
22 except (ValueError, TypeError) as e:
23 fallidos.append({"fila": i, "error": str(e), "registro": reg})
24
25 return exitosos, fallidos
26
27# Datos con problemas reales
28ventas_sucias = [
29 {"producto": "Laptop", "precio": "1299.99", "cantidad": "2"},
30 {"producto": "Monitor", "precio": "N/A"}, # precio no numérico
31 {"precio": "50.00"}, # falta producto
32 {"producto": "Teclado", "precio": "79.99", "cantidad": "3"},
33 {"producto": "Cable", "precio": "-5.00"}, # precio negativo
34]
35
36ok, errores = procesar_ventas(ventas_sucias)
37print(f"\n✓ Procesados: {len(ok)}")
38print(f"✗ Con errores: {len(errores)}")
39for err in errores:
40 print(f" Fila {err['fila']}: {err['error']}")

Patrón profesional: nunca dejes que un registro malo pare todo el pipeline.

Consejo de senior: los errores son DATOS. Guárdalos, no solo los imprimas. En producción, escribes los registros fallidos en un archivo aparte (dead letter queue) para investigarlos después. Nunca los pierdes.

Mi regla personal: si un pipeline procesa < 95% de los registros exitosamente, algo sistémico está mal — no sigas procesando, para y alerta. Si es > 99%, los fallos son ruido esperado. Entre 95-99%, investiga pero no pares.

Nunca pierdas registros buenos por culpa de unos pocos malos.

## ejercicios

[01]

Limpiar precios sucios

Tienes una lista de precios que vienen como strings, algunos inválidos. Usa try/except para convertir los válidos a float e ignorar los inválidos. Reporta cuántos se procesaron y cuántos fallaron.

Cargando editor...
[02]

Lectura segura de archivos

Crea una función leer_json_seguro que intente leer un archivo JSON. Si no existe, devuelve un diccionario vacío. Si el JSON es inválido, devuelve None y muestra el error.

Cargando editor...
[03]

Pipeline tolerante a fallos

Procesa una lista de registros de empleados. Algunos tienen datos faltantes o inválidos. Tu pipeline debe procesar todo lo posible, acumular errores y generar un reporte final.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...