Encadenamiento de excepciones en Python: raise from y preservar el contexto

Cuando una excepción ocurre mientras se maneja otra, Python guarda el contexto automáticamente. La sintaxis raise X from Y permite hacer ese encadenamiento explícito y controlado, comunicando tanto el error nuevo como su causa original.

__context__ vs __cause__: la diferencia

# __context__: encadenamiento implícito (automático)
try:
    int("abc")
except ValueError:
    raise RuntimeError("fallo al parsear")

# Traceback mostrará:
# ValueError: invalid literal...
# During handling of the above exception, another exception occurred:
# RuntimeError: fallo al parsear
# __cause__: encadenamiento explícito con 'from'
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("fallo al parsear") from e

# Traceback mostrará:
# ValueError: invalid literal...
# The above exception was the direct cause of the following exception:
# RuntimeError: fallo al parsear

raise X from None: suprimir el contexto

A veces el contexto original es un detalle de implementación que no debe filtrarse al usuario:

class ErrorConexion(Exception):
    pass

def conectar_base_datos(dsn):
    try:
        import psycopg2
        return psycopg2.connect(dsn)
    except Exception as e:
        # Suprimimos el contexto: el usuario no necesita ver el error
        # interno de psycopg2; solo sabe que falló la conexión
        raise ErrorConexion(f"No se pudo conectar: {dsn}") from None

try:
    conectar_base_datos("postgresql://localhost/midb")
except ErrorConexion as e:
    print(e)  # No se pudo conectar: postgresql://localhost/midb

Ejemplo real: parseo de fechas

from datetime import datetime

class ErrorFechaInvalida(ValueError):
    def __init__(self, texto, formatos):
        self.texto    = texto
        self.formatos = formatos
        super().__init__(
            f"'{texto}' no coincide con ningún formato: {formatos}"
        )

def parsear_fecha(texto, formatos=("%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y")):
    for fmt in formatos:
        try:
            return datetime.strptime(texto, fmt)
        except ValueError:
            continue
    raise ErrorFechaInvalida(texto, formatos)

try:
    parsear_fecha("15-13-2026")  # mes 13 no existe
except ErrorFechaInvalida as e:
    print(e)

Ejemplo real: cliente HTTP con jerarquía

import urllib.request
import json

class ErrorHTTP(Exception):
    pass

class ErrorRed(ErrorHTTP):
    pass

class ErrorRespuesta(ErrorHTTP):
    def __init__(self, codigo, url):
        self.codigo = codigo
        super().__init__(f"HTTP {codigo} al acceder a {url}")

def obtener_json(url):
    try:
        with urllib.request.urlopen(url, timeout=5) as resp:
            datos = resp.read()
    except OSError as e:
        raise ErrorRed(f"No se pudo conectar a {url}") from e

    try:
        return json.loads(datos)
    except json.JSONDecodeError as e:
        raise ErrorRespuesta(200, url) from e

try:
    datos = obtener_json("http://ejemplo.invalido/api")
except ErrorRed as e:
    print(f"Problema de red: {e}")
    print(f"Causa: {e.__cause__}")
except ErrorRespuesta as e:
    print(f"Error HTTP {e.codigo}")

Inspeccionar la cadena de excepciones

def cadena_completa(exc):
    partes = []
    while exc:
        partes.append(f"{type(exc).__name__}: {exc}")
        exc = exc.__cause__ or exc.__context__
    return " ? ".join(reversed(partes))

try:
    try:
        raise ValueError("valor incorrecto")
    except ValueError as e:
        raise RuntimeError("fallo de procesamiento") from e
except RuntimeError as e:
    print(cadena_completa(e))
# ValueError: valor incorrecto ? RuntimeError: fallo de procesamiento

Usa raise X from Y cuando quieras documentar explícitamente la causa de un error, especialmente al cruzar capas de abstracción. Usa raise X from None cuando los detalles de implementación interna no deben exponerse al consumidor de tu librería.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP