Manejo Avanzado de Errores y Excepciones en Python
Un buen manejo de errores hace la diferencia entre un programa que falla silenciosamente y uno que falla de forma predecible, informativa y recuperable. Python ofrece un sistema de excepciones rico que va mucho más allá del simple try/except.
Estructura completa de try/except/else/finally
def leer_configuracion(ruta: str) -> dict:
try:
with open(ruta, 'r', encoding='utf-8') as f:
import json
datos = json.load(f)
except FileNotFoundError:
# Solo se ejecuta si el archivo no existe
print(f"Archivo no encontrado: {ruta}")
return {}
except json.JSONDecodeError as e:
# La variable 'e' contiene detalles del error
print(f"JSON inválido en {ruta}: {e.msg} (línea {e.lineno})")
return {}
except (PermissionError, OSError) as e:
# Capturar múltiples excepciones en una sola cláusula
print(f"Error de acceso: {e}")
raise # re-lanza la misma excepción
else:
# Se ejecuta SOLO si no hubo excepciones
print(f"Configuración cargada: {len(datos)} claves")
return datos
finally:
# SIEMPRE se ejecuta (con o sin excepción)
print("Intento de lectura completado")
Jerarquía de excepciones Python
BaseException
├── SystemExit ← sys.exit()
├── KeyboardInterrupt ← Ctrl+C
├── GeneratorExit
└── Exception ← la base de casi todo
├── ValueError
├── TypeError
├── AttributeError
├── NameError
├── IndexError
├── KeyError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── RuntimeError
├── StopIteration
└── ...
Regla: captura siempre la excepción más específica posible.
except Exception:oculta bugs;except BaseException:también capturaKeyboardInterruptySystemExit.
Excepciones personalizadas
# Jerarquía de excepciones de dominio
class AppError(Exception):
"""Base de todas las excepciones de nuestra aplicación."""
def __init__(self, mensaje: str, codigo: int = 0):
super().__init__(mensaje)
self.codigo = codigo
def __str__(self) -> str:
return f"[{self.codigo}] {super().__str__()}"
class ValidationError(AppError):
"""Error de validación de datos de entrada."""
def __init__(self, campo: str, valor, razon: str):
super().__init__(f"Campo '{campo}': {razon}", codigo=400)
self.campo = campo
self.valor = valor
class ConversionError(AppError):
"""Error durante la conversión de archivos."""
def __init__(self, formato_src: str, formato_dst: str, detalle: str):
msg = f"No se puede convertir {formato_src}→{formato_dst}: {detalle}"
super().__init__(msg, codigo=500)
self.formato_src = formato_src
self.formato_dst = formato_dst
# Uso
def validar_email(email: str) -> str:
import re
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
raise ValidationError('email', email, 'formato inválido')
return email.lower()
try:
validar_email("no-es-un-email")
except ValidationError as e:
print(f"Validación fallida: {e}")
print(f"Campo: {e.campo}, Código: {e.codigo}")
Encadenamiento de excepciones
# raise ... from ... — encadenamiento explícito
def conectar_bd(host: str, puerto: int):
import socket
try:
s = socket.create_connection((host, puerto), timeout=5)
return s
except OSError as original:
raise ConversionError('db', 'host', f"No se pudo conectar a {host}:{puerto}") \
from original
# El traceback mostrará ambas: la causa (OSError) y la nueva (ConversionError)
# raise ... from None — suprimir la causa original (útil cuando reanulas)
def parsear_entero(texto: str) -> int:
try:
return int(texto)
except ValueError:
raise ValidationError('numero', texto, 'debe ser un entero') from None
# Inspeccionar cadena de excepciones
try:
conectar_bd("db.ejemplo.com", 5432)
except ConversionError as e:
print(f"Error: {e}")
if e.__cause__:
print(f"Causa original: {e.__cause__}")
Context managers para limpieza garantizada
from contextlib import contextmanager, suppress
import tempfile
import os
@contextmanager
def archivo_temporal(sufijo: str = '.tmp'):
"""Crea un archivo temporal y lo borra al salir del bloque."""
fd, ruta = tempfile.mkstemp(suffix=sufijo)
try:
os.close(fd)
yield ruta
finally:
if os.path.exists(ruta):
os.unlink(ruta)
print(f"Temporal eliminado: {ruta}")
# Uso
with archivo_temporal('.json') as tmp:
with open(tmp, 'w') as f:
f.write('{"clave": "valor"}')
print(f"Usando temporal: {tmp}")
# El archivo ya fue eliminado aquí
# suppress — ignorar excepciones específicas
with suppress(FileNotFoundError):
os.remove('archivo_inexistente.txt')
# Equivalente a try/except FileNotFoundError: pass
# contextlib.nullcontext — placeholder condicional
def procesar(archivo, modo_debug: bool = False):
ctx = open('debug.log', 'w') if modo_debug else suppress()
with ctx as log:
resultado = archivo.read()
if log:
log.write(resultado)
return resultado
ExceptionGroup (Python 3.11+)
# ExceptionGroup agrupa múltiples excepciones que ocurren "a la vez"
# (común en asyncio TaskGroup o procesamiento paralelo)
def procesar_lote(items: list) -> list:
errores = []
resultados = []
for item in items:
try:
resultados.append(int(item))
except (ValueError, TypeError) as e:
errores.append(e)
if errores:
raise ExceptionGroup("Errores en lote", errores)
return resultados
# except* — captura excepciones específicas dentro del grupo
try:
procesar_lote(["1", "dos", "3", None, "5"])
except* ValueError as eg:
print(f"ValueError(s) en {len(eg.exceptions)} items")
for e in eg.exceptions:
print(f" - {e}")
except* TypeError as eg:
print(f"TypeError(s): {eg.exceptions}")
Módulo traceback — inspeccionar y formatear errores
import traceback
import sys
def funcion_que_falla():
x = {}
return x['clave_inexistente']
# Capturar el traceback completo como string
try:
funcion_que_falla()
except Exception:
tb_str = traceback.format_exc()
print("Traceback capturado:")
print(tb_str)
# Parsear campos del traceback
exc_type, exc_val, exc_tb = sys.exc_info()
for frame in traceback.extract_tb(exc_tb):
print(f" {frame.filename}:{frame.lineno} en {frame.name}")
print(f" → {frame.line}")
# Loggear traceback sin re-lanzar
import logging
log = logging.getLogger(__name__)
try:
funcion_que_falla()
except Exception:
log.exception("Error procesando la solicitud") # incluye traceback automáticamente
Logging estructurado de errores
import logging
import json
import traceback
from datetime import datetime, timezone
class JSONFormatter(logging.Formatter):
"""Formateador que emite cada log como una línea JSON."""
def format(self, record: logging.LogRecord) -> str:
entry = {
'timestamp': datetime.now(timezone.utc).isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
}
if record.exc_info:
entry['exception'] = {
'type': record.exc_info[0].__name__,
'value': str(record.exc_info[1]),
'traceback': traceback.format_exception(*record.exc_info),
}
if hasattr(record, 'extra'):
entry.update(record.extra)
return json.dumps(entry, ensure_ascii=False)
# Configuración
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
log = logging.getLogger('mi_app')
log.addHandler(handler)
log.setLevel(logging.DEBUG)
# Uso
try:
raise ValidationError('edad', -5, 'debe ser positiva')
except ValidationError as e:
log.error(
"Validación fallida",
exc_info=True,
extra={'extra': {'campo': e.campo, 'codigo': e.codigo}}
)
Patrón Result — errores como valores (sin excepciones)
from dataclasses import dataclass
from typing import Generic, TypeVar, Union
T = TypeVar('T')
E = TypeVar('E', bound=Exception)
@dataclass
class Ok(Generic[T]):
value: T
def is_ok(self) -> bool: return True
@dataclass
class Err(Generic[E]):
error: E
def is_ok(self) -> bool: return False
Result = Union[Ok[T], Err[E]]
def parsear_edad(texto: str) -> Result:
try:
edad = int(texto)
if edad < 0 or edad > 150:
return Err(ValidationError('edad', edad, 'fuera de rango'))
return Ok(edad)
except ValueError:
return Err(ValidationError('edad', texto, 'no es un entero'))
# Uso
resultado = parsear_edad("25")
if resultado.is_ok():
print(f"Edad válida: {resultado.value}")
else:
print(f"Error: {resultado.error}")
resultado2 = parsear_edad("abc")
match resultado2:
case Ok(value=v):
print(f"OK: {v}")
case Err(error=e):
print(f"Fallo: {e}")
Buenas prácticas
- Captura la excepción más específica primero — las cláusulas se evalúan en orden y la primera que encaja gana.
- Nunca hagas
except Exception: pass— oculta errores reales. Si necesitas suprimir, usacontextlib.suppress(TipoEspecifico). - Usa
raise ... from originalpara preservar el contexto de causalidad en re-lanzamientos; usaraise ... from Nonesolo cuando el error original es un detalle de implementación irrelevante. log.exception()en lugar delog.error()cuando quieres el traceback automáticamente.- Define jerarquías de excepciones propias para que los consumidores puedan capturar a diferentes niveles de granularidad (
except AppErrorvsexcept ValidationError). - El bloque
elsedel try/except es semánticamente más claro que colocar código de "éxito" al final deltry— usarlo señala intención.
Conversiones relacionadas
Conversiones frecuentes del catálogo: