Manejo de errores en Python: try, except, else, finally y excepciones propias
- Por qué los errores en Python son algo bueno
- try / except: capturar errores con precisión
- Capturar múltiples excepciones
- else y finally: completar el bloque
- raise: lanzar excepciones intencionadamente
- Excepciones personalizadas
- with: el context manager como finally automático
- EAFP vs LBYL: el estilo Python de manejar errores
- Programa completo: lector de CSV robusto
- Errores clásicos con excepciones
- Preguntas frecuentes
Todo programa que hace algo real — leer archivos, llamar a APIs, pedir datos al usuario — puede encontrarse con situaciones inesperadas. El archivo no existe. El servidor no responde. El usuario teclea texto donde se esperaba un número. Python tiene un sistema para manejar esas situaciones de forma controlada, sin que el programa reviente con un mensaje de error incomprensible para el usuario final.
Las excepciones no son el enemigo. Son mensajes de Python diciéndote exactamente qué salió mal y dónde. Aprender a capturarlas, interpretarlas y lanzarlas en el momento correcto es lo que separa un script frágil de un programa robusto.
🛡️ try / except: capturar errores con precisión
La estructura básica: el código que puede fallar va en el bloque try. Si ocurre una excepción, Python salta al bloque except correspondiente en lugar de detener el programa.
# Sin manejo de errores — el programa muere:
numero = int(input("Introduce un número: ")) # si el usuario escribe "hola" → ValueError
# Con manejo de errores:
try:
numero = int(input("Introduce un número: "))
print(f"El doble es {numero * 2}")
except ValueError:
print("Eso no es un número válido.")
# El programa sigue ejecutándose después del except
print("Gracias por participar.")
La clave es capturar la excepción más específica posible. Si sabes que puede ser un ValueError, captura ValueError, no Exception a ciegas. Así no silencias errores que sí deberían propagarse.
# Acceder al objeto excepción con 'as e':
try:
resultado = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}") # Error: division by zero
print(type(e).__name__) # ZeroDivisionError
# Obtener información del error:
try:
lista = [1, 2, 3]
print(lista[10])
except IndexError as e:
print(f"Índice inválido: {e}")
# Índice inválido: list index out of range
⚡ Capturar múltiples excepciones
Puedes encadenar varios except, cada uno manejando un tipo distinto de error. Python los evalúa en orden y ejecuta el primero que coincida.
def dividir(a, b):
try:
resultado = a / b
return resultado
except ZeroDivisionError:
print("No se puede dividir por cero.")
return None
except TypeError as e:
print(f"Tipo incorrecto: {e}")
return None
dividir(10, 2) # → 5.0
dividir(10, 0) # → "No se puede dividir por cero."
dividir(10, "dos") # → "Tipo incorrecto: unsupported operand type(s)..."
# Capturar varios tipos en un solo except:
try:
valor = int(input("Número: "))
print(100 / valor)
except (ValueError, ZeroDivisionError) as e:
print(f"Entrada inválida: {e}")
# Fallback genérico al final (con logging):
import logging
try:
resultado = operacion_compleja()
except ValueError as e:
print(f"Valor incorrecto: {e}")
except ConnectionError as e:
print(f"Error de conexión: {e}")
except Exception as e:
logging.error(f"Error inesperado: {e}", exc_info=True)
raise # re-lanzar para no silenciarlo
Exception primero, capturará todo y los except siguientes nunca se ejecutarán.
🔄 else y finally: completar el bloque
else se ejecuta solo si el bloque try terminó sin excepción. finally se ejecuta siempre, con o sin error. Son opcionales pero tienen un propósito claro.
try:
numero = int(input("Número positivo: "))
if numero <= 0:
raise ValueError("Debe ser positivo")
resultado = 100 / numero
except ValueError as e:
print(f"Error de valor: {e}")
except ZeroDivisionError:
print("División por cero.")
else:
# Solo si try terminó sin excepciones:
print(f"Resultado: {resultado}")
print("Todo correcto.")
finally:
# SIEMPRE, pase lo que pase:
print("Bloque ejecutado (finally).")
finally para liberar recursos
archivo = None
try:
archivo = open("datos.txt", "r")
contenido = archivo.read()
procesar(contenido)
except FileNotFoundError:
print("El archivo no existe.")
except PermissionError:
print("Sin permisos de lectura.")
finally:
if archivo:
archivo.close() # siempre se cierra, haya error o no
🚀 raise: lanzar excepciones intencionadamente
Usas raise cuando tu código detecta una condición que no puede manejar y quiere comunicársela a quien te llamó.
def calcular_imc(peso, altura):
if altura <= 0:
raise ValueError(f"La altura debe ser positiva, recibido: {altura}")
if peso <= 0:
raise ValueError(f"El peso debe ser positivo, recibido: {peso}")
return round(peso / altura ** 2, 2)
# Quién llama puede manejar el error:
try:
imc = calcular_imc(70, -1.75)
except ValueError as e:
print(f"Datos inválidos: {e}")
# Re-lanzar después de logging:
try:
resultado = operacion_critica()
except Exception as e:
logging.error(f"Falló operación crítica: {e}")
raise # propaga la excepción original sin modificarla
# Lanzar una excepción diferente con contexto:
try:
datos = json.loads(texto_recibido)
except json.JSONDecodeError as e:
raise ValueError(f"El texto no es JSON válido: {texto_recibido[:50]}") from e
🏷️ Excepciones personalizadas
Crear tus propias excepciones hace el código más expresivo. Quien use tu módulo puede capturar exactamente el tipo de error que le interesa manejar.
# Hereda de Exception o de una subclase apropiada
class SaldoInsuficienteError(Exception):
"""Se lanza cuando el saldo no alcanza para la operación."""
def __init__(self, saldo_actual, cantidad_requerida):
self.saldo_actual = saldo_actual
self.cantidad_requerida = cantidad_requerida
super().__init__(
f"Saldo insuficiente: tienes {saldo_actual}€, "
f"necesitas {cantidad_requerida}€"
)
class CuentaBloqueadaError(Exception):
"""Se lanza cuando la cuenta está bloqueada."""
pass
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self._saldo = saldo_inicial
self._bloqueada = False
def retirar(self, cantidad):
if self._bloqueada:
raise CuentaBloqueadaError(f"Cuenta de {self.titular} bloqueada.")
if cantidad > self._saldo:
raise SaldoInsuficienteError(self._saldo, cantidad)
self._saldo -= cantidad
return self._saldo
# Uso:
cuenta = CuentaBancaria("Ana", 100)
try:
cuenta.retirar(150)
except SaldoInsuficienteError as e:
print(f"No se pudo retirar: {e}")
print(f" Faltan {e.cantidad_requerida - e.saldo_actual}€")
except CuentaBloqueadaError as e:
print(f"Operación denegada: {e}")
🔒 with: el context manager como finally automático
El patrón with garantiza que los recursos se liberan correctamente sin necesidad de escribir finally explícito. Es la forma recomendada de trabajar con archivos, conexiones y cualquier recurso que deba cerrarse.
# Sin with (verboso y propenso a olvidar el cierre):
archivo = open("datos.txt")
try:
contenido = archivo.read()
finally:
archivo.close()
# Con with (equivalente, más limpio):
with open("datos.txt") as archivo:
contenido = archivo.read()
# archivo.close() se llama automáticamente al salir del bloque
# Escritura segura:
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Primera línea\n")
f.write("Segunda línea\n")
# Si hay error al escribir, el archivo se cierra de todas formas
# Múltiples recursos en un solo with (Python 3.10+):
with open("entrada.txt") as f_in, open("salida.txt", "w") as f_out:
for linea in f_in:
f_out.write(linea.upper())
# with en manejo de errores:
try:
with open("datos.csv") as f:
datos = f.readlines()
except FileNotFoundError:
print("El archivo no existe.")
datos = []
🐍 EAFP vs LBYL: el estilo Python de manejar errores
Python promueve el estilo EAFP (Easier to Ask Forgiveness than Permission): intenta la operación y captura el error si ocurre. El estilo contrario, LBYL (Look Before You Leap), comprueba antes de actuar. En Python el EAFP suele producir código más limpio.
diccionario = {"nombre": "Ana", "edad": 28}
# LBYL — comprobar antes:
if "email" in diccionario:
email = diccionario["email"]
else:
email = "sin email"
# EAFP — intentar y capturar:
try:
email = diccionario["email"]
except KeyError:
email = "sin email"
# EAFP aún más idiomático para este caso:
email = diccionario.get("email", "sin email")
# Otro ejemplo: abrir archivo
# LBYL:
import os
if os.path.exists("config.json"):
with open("config.json") as f:
config = json.load(f)
# EAFP:
try:
with open("config.json") as f:
config = json.load(f)
except FileNotFoundError:
config = {} # configuración por defecto
🛠️ Programa completo: lector de CSV robusto
import csv
import logging
logging.basicConfig(level=logging.WARNING,
format="%(levelname)s: %(message)s")
class ErrorCsvInvalido(ValueError):
"""CSV con estructura incorrecta o datos corruptos."""
pass
def leer_empleados(ruta_csv):
"""
Lee un CSV de empleados y devuelve una lista de dicts.
Columnas esperadas: nombre, departamento, salario
"""
empleados = []
columnas_req = {"nombre", "departamento", "salario"}
try:
with open(ruta_csv, encoding="utf-8") as f:
lector = csv.DictReader(f)
# Verificar columnas
if not columnas_req.issubset(set(lector.fieldnames or [])):
faltantes = columnas_req - set(lector.fieldnames or [])
raise ErrorCsvInvalido(
f"Columnas faltantes: {', '.join(faltantes)}"
)
for numero_fila, fila in enumerate(lector, start=2):
try:
salario = float(fila["salario"])
if salario < 0:
raise ValueError("El salario no puede ser negativo")
empleados.append({
"nombre": fila["nombre"].strip(),
"departamento": fila["departamento"].strip(),
"salario": salario,
})
except ValueError as e:
logging.warning(f"Fila {numero_fila} omitida: {e}")
continue # salta filas con datos inválidos, no para todo
except FileNotFoundError:
print(f"Archivo no encontrado: {ruta_csv}")
return []
except PermissionError:
print(f"Sin permisos para leer: {ruta_csv}")
return []
except ErrorCsvInvalido as e:
print(f"Estructura CSV inválida: {e}")
return []
else:
print(f"Leídos {len(empleados)} empleados correctamente.")
return empleados
def resumen_por_departamento(empleados):
"""Calcula salario medio por departamento."""
por_dept = {}
for e in empleados:
dept = e["departamento"]
if dept not in por_dept:
por_dept[dept] = []
por_dept[dept].append(e["salario"])
return {
dept: round(sum(salarios) / len(salarios), 2)
for dept, salarios in por_dept.items()
}
# Uso
empleados = leer_empleados("empleados.csv")
if empleados:
resumen = resumen_por_departamento(empleados)
print("\nSalario medio por departamento:")
for dept, media in sorted(resumen.items()):
print(f" {dept:<20} {media:>10.2f} €")
🐛 Errores clásicos con excepciones
1. Capturar Exception demasiado pronto y silenciar el error
# ❌ El error desaparece sin dejar rastro
try:
resultado = calcular()
except Exception:
pass # ← nunca hagas esto
# ✅ Al menos registrar el error
except Exception as e:
logging.error(f"Error en calcular(): {e}", exc_info=True)
2. Poner demasiado código en el try
# ❌ Difícil saber qué línea lanzó el error
try:
datos = leer_archivo(ruta)
datos = procesar(datos)
guardar(datos)
notificar_usuario()
except Exception as e:
print(f"Algo falló: {e}")
# ✅ try acotado al código que puede fallar, con manejo específico
try:
datos = leer_archivo(ruta)
except FileNotFoundError:
print("Archivo no encontrado."); return
datos = procesar(datos) # si esto falla, es un bug, que propague
guardar(datos)
3. Olvidar que finally no devuelve valor útil
# ❌ El return del finally sobreescribe el del try
def obtener():
try:
return "valor_real"
finally:
return "siempre_esto" # ← siempre devuelve esto, oculta el valor real
# ✅ finally solo para limpieza, nunca para lógica de negocio
def obtener():
try:
resultado = calcular()
return resultado
finally:
limpiar_recursos() # limpieza, sin return
✅ Resumen y próximos pasos
try/except captura errores sin detener el programa. Captura siempre la excepción más específica. else se ejecuta solo si no hubo error. finally siempre se ejecuta — ideal para liberar recursos. raise lanza excepciones intencionadamente, con tu propio mensaje. Las excepciones personalizadas hacen el código más expresivo. Y with es la forma idiomática de gestionar recursos sin olvidar el cierre.
La siguiente lección: archivos y rutas — leer y escribir ficheros de texto, CSV y JSON con Python.
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Manejo de errores en Python: try, except, else, finally y excepciones propias
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Manejo de errores en Python: try, except, else, finally y excepciones propias? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!