Jerarquías de Excepciones Personalizadas
Las excepciones de dominio agrupan fallos relacionados para que los llamadores puedan capturarlos con la granularidad adecuada. Un árbol poco profundo bajo una base específica del paquete es mejor que docenas de subclases Exception no relacionadas.
Receta
class AppError(Exception):
"""Base para todos los errores del servicio."""
class ValidationError(AppError):
def __init__(self, field: str, message: str) -> None:
self.field = field
super().__init__(f"{field}: {message}")Cuándo usar esto:
- Las capas HTTP/API mapean tipos de excepción a códigos de estado
- Los llamadores necesitan capturar todos los errores de pago pero no todos los errores de la aplicación
- Las bibliotecas exportan una base de excepciones pública estable
- Las métricas/alertas se etiquetan por clase de excepción
- Las pruebas afirman tipos de fallo específicos
Ejemplo de Trabajo
class BillingError(Exception):
"""Se lanza para fallos del subsistema de facturación."""
class CardDeclined(BillingError):
def __init__(self, last4: str, reason: str) -> None:
self.last4 = last4
self.reason = reason
super().__init__(f"tarjeta *{last4} declinada: {reason}")
class InsufficientFunds(CardDeclined):
pass
class InvoiceError(BillingError):
pass
def charge(card_last4: str, amount: float) -> None:
if amount > 1000:
raise InsufficientFunds(card_last4, "fondos insuficientes")
if amount < 0:
raise InvoiceError("monto negativo")
def handle_payment() -> str:
try:
charge("4242", 2000)
except CardDeclined as exc:
return f"reintentar con una tarjeta nueva ({exc.reason})"
except BillingError:
return "facturación no disponible"
return "ok"
print(handle_payment())Lo que esto demuestra:
- La base del paquete
BillingErrorcaptura cualquier fallo de facturación CardDeclinedañade campos para los manejadores y los registros- Las subclases se especializan sin duplicar la lógica del manejador
- Ramas separadas para errores de facturación recuperables vs genéricos
Análisis Profundo
Cómo Funciona
- Hereda de
Exception(no deBaseException) para errores de aplicación - Una base a nivel de módulo o paquete por contexto delimitado
- Adjunta atributos en
__init__antes desuper().__init__(message) - Mantén la jerarquía poco profunda (base + 2-3 niveles)
- Documenta qué excepciones cruzan los límites del paquete
Directrices de Jerarquía
| Capa | Base | Ejemplo |
|---|---|---|
| Biblioteca | LibraryError | ConnectionTimeout |
| Dominio de la aplicación | AppError | OrderNotFound |
| Adaptador HTTP | Mapear a estado | 404, 422, 503 |
Trampas
except Exceptioncomo "catch-all" enmascara errores - los manejadores demasiado amplios ocultanKeyboardInterruptsi se definen incorrectamente. Solución: captura tu base, luego tipos específicos.- Árboles de herencia profundos - comportamiento duplicado entre hermanos. Solución: composición de campos, no pilas de subclases.
- Errores solo de texto - análisis del texto del mensaje en los manejadores. Solución: atributos estructurados (
field,code). - Fugas de excepciones internas - los errores del controlador de la base de datos llegan a los clientes de la API. Solución: envolver en el límite con
from exc. - Clases de excepción vacías sin documentación - los mantenedores adivinan cuándo lanzar. Solución: docstring por clase con condiciones de lanzamiento.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Códigos estilo errno | API estable legible por máquina | Servicios internos de Python |
ValidationError de Pydantic | Solo análisis de entrada | Violaciones de reglas de negocio |
Tipos Result | Módulos puramente funcionales | El framework espera excepciones |
Preguntas Frecuentes
¿Cuántas excepciones base debe tener una aplicación?
Una por contexto delimitado (facturación, autenticación, inventario) más una base opcional para toda la aplicación si necesitas una captura universal en main().
¿Debo heredar de ValueError?
Raramente para errores de dominio. Hereda de tu base de dominio para que los manejadores no capturen ValueError no relacionados.
¿Cómo usan las jerarquías los frameworks HTTP?
Registra manejadores: @app.exception_handler(CardDeclined) devuelve 402 con cuerpo estructurado.
¿Son las excepciones dataclass aceptables?
Sí en 3.11+ con dataclass en subclases de Exception - asegúrate de que __init__ llama a super correctamente.
¿Cómo nombro las excepciones?
Frases nominales para el estado (UserNotFound) o tiempo pasado de verbos para eventos (PaymentDeclined).
¿Deben las bibliotecas reexportar excepciones?
Sí en __all__ - la superficie de la API pública incluye tipos de excepción estables.
¿Cómo migro de Exception genérica?
Introduce una base, envuelve los raise en los límites, y desaprueba Exception desnuda en lanzamientos sucesivos.
¿Qué pasa con ExceptionGroup?
Los fallos concurrentes usan grupos; las jerarquías de dominio todavía se aplican dentro de las excepciones hoja.
¿Necesito códigos únicos?
Añade .code: str cuando los clientes se ramifican programáticamente; el tipo de clase solo suele ser suficiente en servicios de Python.
¿Cómo pruebo las jerarquías?
pytest.raises(BillingError) y assert isinstance(exc.value, CardDeclined) para especialización.
Relacionado
- Lanzamiento y Encadenamiento - envolver con
from - Grupos de Excepciones y except* - errores concurrentes
- Conceptos Básicos de Excepciones - orden de captura
- Mejores Prácticas de Errores y Registro - fallar ruidosamente
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.