CAP 07 · LEC 02·Manejo de errores

raise y propagación: lanzar excepciones conscientemente

raise es tu forma de decir «aquí hay un error y no sé cómo resolverlo aquí». La propagación permite que las excepciones suban por el stack hasta encontrar quien las maneje — o terminar el programa.

● INTERMEDIO7 min lectura4 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

raise con excepción nueva

raise lanza una excepción desde cualquier punto del código. El stack se desenrolla hacia arriba hasta que alguien la captura con except, o el programa termina con un traceback.

def validar_edad(edad: int) -> None: if edad < 0: raise ValueError(f"La edad no puede ser negativa: {edad}") if edad > 150: raise ValueError(f"Edad inverosímil: {edad}") def crear_usuario(nombre: str, edad: int) -> dict: if not nombre.strip(): raise ValueError("El nombre no puede estar vacío") validar_edad(edad) # puede lanzar ValueError return {"nombre": nombre, "edad": edad} # Uso correcto ✅ usuario = crear_usuario("Ana", 28) print(usuario) # {'nombre': 'Ana', 'edad': 28} # Error capturado ✅ try: crear_usuario("", 28) except ValueError as e: print(f"Error: {e}") # Error: El nombre no puede estar vacío # Error no capturado — sube por el stack ⬆️ # crear_usuario("Carlos", -5) # ValueError: La edad no puede ser negativa: -5
Salida{'nombre': 'Ana', 'edad': 28} Error: El nombre no puede estar vacío
raise con clase vs con instancia

raise ValueError("mensaje") — con instancia (preferido, más descriptivo). raise ValueError — con clase (crea la instancia sin mensaje). Solo úsalo cuando el tipo es suficientemente descriptivo por sí solo.

raise desde dentro de except — encadenar excepciones

Cuando capturas una excepción y decides relanzar una diferente (más abstracta o más específica para tu dominio), usa raise NuevaExcepcion from excepcion_original. Esto preserva el contexto original en el traceback.

import json class ConfigError(Exception): """Error de configuración de la aplicación.""" def cargar_config(ruta: str) -> dict: try: with open(ruta) as f: return json.load(f) except FileNotFoundError as e: # Convertimos FileNotFoundError en ConfigError de dominio raise ConfigError(f"Archivo de config no encontrado: {ruta}") from e except json.JSONDecodeError as e: # El 'from e' preserva la causa original en el traceback raise ConfigError(f"Config con formato inválido en {ruta}") from e try: config = cargar_config("config.json") except ConfigError as e: print(f"Error de config: {e}") # El __cause__ apunta al error original if e.__cause__: print(f"Causa: {type(e.__cause__).__name__}: {e.__cause__}")
SalidaError de config: Archivo de config no encontrado: config.json Causa: FileNotFoundError: [Errno 2] No such file or directory: 'config.json'
raise X from Y vs raise X from None

raise NuevaExc from original — preserva la causa (recomendado). El traceback muestra «The above exception was the direct cause of…».

raise NuevaExc from None — suprime la cadena. Útil cuando el error original expone detalles de implementación que no quieres mostrar (contraseñas, rutas internas, etc.).

raise sin argumentos — re-raise

raise sin argumentos dentro de un except relanza la excepción activa. Es útil cuando quieres hacer algo adicional (logging, métricas) y luego dejar que el error siga propagándose.

import logging logging.basicConfig(level=logging.ERROR) logger = logging.getLogger(__name__) def pago(monto: float, metodo: str) -> bool: try: if monto <= 0: raise ValueError(f"Monto inválido: {monto}") if metodo not in ("tarjeta", "transferencia"): raise ValueError(f"Método no soportado: {metodo}") return True except ValueError as e: # Loggeamos el error con contexto adicional logger.error("Pago rechazado — monto=%.2f método=%s error=%s", monto, metodo, e) raise # re-lanza el ValueError original sin modificarlo # El llamador recibe el error original try: pago(-50, "tarjeta") except ValueError as e: print(f"Capturado en llamador: {e}") # Capturado en llamador: Monto inválido: -50
SalidaERROR:__main__:Pago rechazado — monto=-50.00 método=tarjeta error=Monto inválido: -50 Capturado en llamador: Monto inválido: -50

Cuándo raise y cuándo retornar None

Esta es una de las decisiones de diseño más importantes. La regla general: usa excepciones para situaciones excepcionales, y retorna valores especiales (None, [], {}) cuando la ausencia de un resultado es un caso normal.

Retorna None/vacíoLanza excepción
buscar_usuario(id) → el usuario puede no existirbuscar_usuario(id) → si no existe es un error de lógica
filtrar_por_categoria([]) → lista vacía es válidadividir(a, 0) → la división por cero nunca es válida
config.get('clave', default) → clave puede faltarconfig['clave_requerida'] → debe existir siempre
from typing import Optional # ✅ Retorna None: no encontrar un usuario es un caso normal def buscar_usuario(user_id: int) -> Optional[dict]: usuarios = {1: {"nombre": "Ana"}, 2: {"nombre": "Carlos"}} return usuarios.get(user_id) # None si no existe # ✅ Lanza excepción: los parámetros deben ser válidos siempre def registrar_usuario(email: str, contrasena: str) -> dict: if "@" not in email: raise ValueError(f"Email inválido: {email}") if len(contrasena) < 8: raise ValueError("La contraseña debe tener al menos 8 caracteres") return {"email": email, "activo": True} # Uso usuario = buscar_usuario(99) if usuario is None: print("Usuario no encontrado — flujo normal") try: registrar_usuario("sin-arroba", "123") except ValueError as e: print(f"Error de validación: {e}")
SalidaUsuario no encontrado — flujo normal Error de validación: Email inválido: sin-arroba

Practica