En esta página
Manejo de errores
¿Por qué manejar errores?
En el mundo real, los programas fallan. Los archivos no existen, la red falla, el usuario ingresa datos inválidos, la base de datos se cae. La diferencia entre código de producción y código de juguete está en cómo se manejan esos fallos.
Python usa un mecanismo de excepciones para el manejo de errores. Cuando algo sale mal, Python "lanza" una excepción que, si no se captura, termina el programa con un mensaje de error. Nuestro trabajo es anticipar los fallos y decidir cómo responder a cada uno.
La estructura try / except
# Forma básica
try:
resultado = 10 / 0
except ZeroDivisionError:
print("Error: no se puede dividir entre cero")
# Sin manejo, esto habría impreso:
# ZeroDivisionError: division by zero
# Y habría terminado el programa
# Capturar múltiples tipos de excepción
def convertir_a_int(texto: str) -> int | None:
try:
return int(texto)
except ValueError:
print(f"'{texto}' no es un número válido")
return None
except TypeError:
print("Se esperaba una cadena de texto")
return None
print(convertir_a_int("42")) # 42
print(convertir_a_int("abc")) # Error → None
print(convertir_a_int(None)) # Error → None
# Capturar múltiples excepciones en una línea
try:
valor = int(input("Ingresa un número: "))
except (ValueError, EOFError):
print("Entrada inválida")Acceder al objeto de excepción
El objeto de excepción contiene información sobre el error:
try:
lista = [1, 2, 3]
print(lista[10])
except IndexError as e:
print(f"Error de índice: {e}")
print(f"Tipo: {type(e).__name__}")
# Podemos re-lanzar con más contexto
# raise RuntimeError("Índice fuera de rango en proceso crítico") from eLa estructura completa: try / except / else / finally
import json
def leer_configuración(ruta: str) -> dict:
"""
Lee un archivo de configuración JSON.
Demuestra el uso completo de try/except/else/finally.
"""
archivo = None
try:
# Código que puede fallar
archivo = open(ruta, "r", encoding="utf-8")
datos = json.load(archivo)
except FileNotFoundError:
print(f"Archivo no encontrado: {ruta}")
return {}
except json.JSONDecodeError as e:
print(f"JSON inválido en {ruta}: {e}")
return {}
except PermissionError:
print(f"Sin permisos para leer: {ruta}")
return {}
else:
# Se ejecuta SOLO si no hubo excepción en try
print(f"Configuración cargada exitosamente desde {ruta}")
return datos
finally:
# Se ejecuta SIEMPRE — con o sin excepción
if archivo is not None:
archivo.close()
print("Archivo cerrado")
config = leer_configuración("config.json")Excepciones comunes en Python
ValueError
Se lanza cuando una función recibe el tipo correcto pero un valor inapropiado:
# Casos que lanzan ValueError
int("abc") # No es representable como int
float("infinito") # No es un float válido
math.sqrt(-1) # No definido en reales (usa cmath para complejos)
# Patrón de validación con ValueError
def calcular_descuento(precio: float, porcentaje: float) -> float:
if precio < 0:
raise ValueError(f"El precio no puede ser negativo: {precio}")
if not 0 <= porcentaje <= 100:
raise ValueError(f"El porcentaje debe estar entre 0 y 100: {porcentaje}")
return precio * (1 - porcentaje / 100)
try:
descuento = calcular_descuento(100, 150)
except ValueError as e:
print(f"Error de valor: {e}")TypeError
Se lanza cuando una operación se aplica a un tipo incorrecto:
try:
resultado = "5" + 10 # No se pueden sumar str y int
except TypeError as e:
print(f"TypeError: {e}")
try:
for x in 42: # Los enteros no son iterables
print(x)
except TypeError as e:
print(f"TypeError: {e}")
# Función con validación de tipos
def calcular_promedio(numeros: list[float]) -> float:
if not isinstance(numeros, (list, tuple)):
raise TypeError(f"Se esperaba list o tuple, no {type(numeros).__name__}")
if len(numeros) == 0:
raise ValueError("La lista no puede estar vacía")
return sum(numeros) / len(numeros)KeyError y IndexError
# KeyError — clave no existe en diccionario
config = {"host": "localhost", "puerto": 8080}
try:
timeout = config["timeout"]
except KeyError as e:
print(f"Clave no encontrada: {e}")
timeout = 30 # valor por defecto
# Mejor patrón — usar .get()
timeout = config.get("timeout", 30)
# IndexError — índice fuera de rango
numeros = [1, 2, 3]
try:
print(numeros[10])
except IndexError as e:
print(f"Índice inválido: {e}")FileNotFoundError y OSError
from pathlib import Path
def leer_archivo(ruta: str) -> str:
try:
return Path(ruta).read_text(encoding="utf-8")
except FileNotFoundError:
raise FileNotFoundError(f"El archivo no existe: {ruta}")
except PermissionError:
raise PermissionError(f"Sin permiso para leer: {ruta}")
except OSError as e:
raise OSError(f"Error del sistema al leer {ruta}: {e}") from eLanzar excepciones con raise
def dividir(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("El divisor no puede ser cero")
return a / b
# Re-lanzar la excepción actual (dentro de un except)
def procesar_datos(datos: list[int]) -> list[int]:
try:
return [100 // x for x in datos]
except ZeroDivisionError:
print("Advertencia: se encontró un cero en los datos")
raise # Re-lanza la misma excepción sin perder el traceback
# Re-lanzar como un tipo diferente (encadenamiento implícito)
def cargar_usuario(uid: int) -> dict:
try:
usuarios = {1: {"nombre": "Ana"}}
return usuarios[uid]
except KeyError:
raise ValueError(f"Usuario con ID {uid} no encontrado")Encadenamiento de excepciones
# raise X from Y — encadenamiento explícito (preserva causa original)
def conectar_bd(url: str) -> object:
try:
# Simular fallo de conexión
raise ConnectionRefusedError("Puerto 5432 rechazado")
except ConnectionRefusedError as e:
raise RuntimeError(f"No se pudo conectar a la BD: {url}") from e
try:
conectar_bd("postgresql://localhost/midb")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Causado por: {e.__cause__}")
# raise X from None — suprimir la cadena (cuando no quieres mostrar el original)
def buscar_por_nombre(nombre: str) -> dict:
base_de_datos = {"Ana": {"id": 1}, "Bruno": {"id": 2}}
try:
return base_de_datos[nombre]
except KeyError:
# No queremos revelar que usamos un dict internamente
raise ValueError(f"Usuario no encontrado: {nombre}") from NoneExcepciones personalizadas
Crear una jerarquía de excepciones propias es una buena práctica para aplicaciones grandes:
# Clase base para toda la aplicación
class TiendaError(Exception):
"""Error base de la aplicación de tienda."""
def __init__(self, mensaje: str, codigo: int = 0) -> None:
super().__init__(mensaje)
self.codigo = codigo
def __str__(self) -> str:
return f"[{self.codigo}] {super().__str__()}"
class ProductoNoEncontrado(TiendaError):
"""El producto solicitado no existe."""
pass
class StockInsuficiente(TiendaError):
"""No hay suficiente stock para la cantidad solicitada."""
def __init__(self, producto: str, disponible: int, solicitado: int) -> None:
super().__init__(
f"Stock insuficiente para '{producto}': "
f"{disponible} disponibles, {solicitado} solicitados",
codigo=409
)
self.disponible = disponible
self.solicitado = solicitado
class PrecioInvalido(TiendaError):
"""El precio especificado es inválido."""
pass
# Usar las excepciones personalizadas
class Carrito:
def __init__(self) -> None:
self._items: dict[str, int] = {}
self._catalogo: dict[str, dict] = {
"P001": {"nombre": "Laptop", "precio": 1299.99, "stock": 5},
"P002": {"nombre": "Mouse", "precio": 29.99, "stock": 0},
}
def agregar(self, producto_id: str, cantidad: int) -> None:
if producto_id not in self._catalogo:
raise ProductoNoEncontrado(
f"Producto '{producto_id}' no existe en el catálogo",
codigo=404
)
producto = self._catalogo[producto_id]
if producto["stock"] < cantidad:
raise StockInsuficiente(
producto["nombre"],
disponible=producto["stock"],
solicitado=cantidad
)
self._items[producto_id] = self._items.get(producto_id, 0) + cantidad
producto["stock"] -= cantidad
carrito = Carrito()
try:
carrito.agregar("P001", 3)
print("Producto agregado con éxito")
carrito.agregar("P002", 1) # Sin stock
except StockInsuficiente as e:
print(f"Error de stock: {e}")
print(f" Disponible: {e.disponible}, Solicitado: {e.solicitado}")
except ProductoNoEncontrado as e:
print(f"Error de catálogo: {e}")
except TiendaError as e:
print(f"Error general de tienda: {e}")Contexto con contextlib.suppress
Para casos donde quieres ignorar específicamente ciertos errores:
from contextlib import suppress
# En lugar de:
try:
import ujson as json
except ImportError:
import json
# Para ignorar silenciosamente (usar con cuidado)
with suppress(FileNotFoundError):
Path("temporal.txt").unlink() # Eliminar si existe, ignorar si noJerarquía de excepciones importantes
BaseException
├── SystemExit — sys.exit()
├── KeyboardInterrupt — Ctrl+C
└── Exception — Todas las demás
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ConnectionError
├── ValueError
├── TypeError
├── AttributeError
├── NameError
└── RuntimeErrorResumen
El manejo robusto de errores es la diferencia entre código que falla misteriosamente en producción y código que falla con gracia y mensajes útiles. Captura las excepciones más específicas, usa finally para liberar recursos, y crea jerarquías de excepciones propias para aplicaciones complejas. En la próxima lección aprenderemos a organizar el código en módulos y paquetes.
Inicia sesión para guardar tu progreso