El manejo de errores es parte central de la calidad de código. Una excepción mal capturada puede ocultar fallas, devolver resultados incorrectos o hacer que el programa continúe en un estado inválido.
En este tema veremos smells frecuentes relacionados con excepciones: capturas demasiado genéricas, errores silenciados, try ubicados en lugares incorrectos y mensajes poco útiles. Trabajaremos con ejemplos Python y prácticas seguras.
Un error bien manejado ayuda a saber qué pasó, dónde pasó y qué puede hacer el programa a continuación. Un error mal manejado esconde información o transforma una falla clara en un comportamiento extraño.
El objetivo no es llenar el código de try y except. El objetivo es decidir qué errores podemos resolver localmente y cuáles debemos dejar subir para que otra capa los maneje.
Silenciar una excepción consiste en capturarla y no hacer nada, o devolver un valor que oculta el problema.
def obtener_precio(producto):
try:
return float(producto["precio"])
except Exception:
pass
Esta función puede devolver None sin explicación. Quien la usa no sabe si faltó el precio, si el valor era inválido o si ocurrió otro error.
Conviene capturar solo las excepciones que esperamos y sabemos manejar.
def obtener_precio(producto):
try:
return float(producto["precio"])
except KeyError:
raise ValueError("El producto no tiene precio")
except ValueError:
raise ValueError("El precio del producto no es numérico")
Ahora el error conserva información útil. No se oculta; se transforma en un mensaje más cercano al dominio.
Capturar Exception puede ocultar errores inesperados.
def calcular_total(productos):
try:
return sum(producto["precio"] * producto["cantidad"] for producto in productos)
except Exception:
return 0
Si hay un error en una clave, un tipo incorrecto o una regla mal escrita, la función devuelve 0. Ese resultado puede parecer válido y contaminar cálculos posteriores.
A veces una validación explícita es mejor que capturar cualquier excepción.
def validar_producto(producto):
if "precio" not in producto:
raise ValueError("Falta el precio del producto")
if "cantidad" not in producto:
raise ValueError("Falta la cantidad del producto")
if producto["cantidad"] <= 0:
raise ValueError("La cantidad debe ser positiva")
def calcular_total(productos):
total = 0
for producto in productos:
validar_producto(producto)
total += producto["precio"] * producto["cantidad"]
return total
La validación hace visible la regla y produce errores claros.
Un bloque try muy grande dificulta saber qué operación puede fallar.
def cargar_total(ruta):
try:
archivo = open(ruta, encoding="utf-8")
lineas = archivo.readlines()
valores = [float(linea) for linea in lineas]
total = sum(valores)
archivo.close()
return total
except ValueError:
return 0
La excepción ValueError ocurre al convertir texto a número, no al abrir el archivo. El try debería cubrir la zona precisa.
def convertir_linea_a_float(linea):
try:
return float(linea)
except ValueError as error:
raise ValueError(f"Valor numérico inválido: {linea.strip()}") from error
def cargar_total(ruta):
with open(ruta, encoding="utf-8") as archivo:
valores = [convertir_linea_a_float(linea) for linea in archivo]
return sum(valores)
El manejo de error queda donde ocurre la conversión. Además, with se encarga de cerrar el archivo correctamente.
Cuando transformas una excepción en otra, usar from conserva la causa original.
def obtener_cantidad(producto):
try:
return int(producto["cantidad"])
except KeyError as error:
raise ValueError("Falta la cantidad del producto") from error
except ValueError as error:
raise ValueError("La cantidad debe ser un número entero") from error
Esto ayuda a depurar porque no se pierde la excepción original.
En algunos casos conviene crear una excepción propia para expresar un problema del dominio.
class ProductoInvalidoError(ValueError):
pass
def validar_producto(producto):
if producto["cantidad"] <= 0:
raise ProductoInvalidoError("La cantidad debe ser positiva")
No hace falta crear clases de excepción para todo. Úsalas cuando ayuden a diferenciar errores importantes para la aplicación.
Las excepciones son útiles para situaciones excepcionales. No conviene usarlas como reemplazo de una condición simple.
def obtener_descuento(cliente):
try:
return {"vip": 0.15, "regular": 0.05}[cliente]
except KeyError:
return 0
En este caso, dict.get expresa mejor la intención:
DESCUENTOS = {
"vip": 0.15,
"regular": 0.05,
}
def obtener_descuento(cliente):
return DESCUENTOS.get(cliente, 0)
A veces necesitamos registrar el error y volver a lanzarlo. Eso es distinto de silenciarlo.
import logging
logger = logging.getLogger(__name__)
def procesar_archivo(ruta):
try:
return cargar_total(ruta)
except OSError:
logger.exception("No se pudo procesar el archivo %s", ruta)
raise
logger.exception registra el traceback dentro de un bloque except. Luego raise vuelve a lanzar la excepción original.
Supón que en ventas_demo agregamos una validación de productos:
def validar_producto(producto):
if "precio" not in producto:
raise ValueError("Falta el precio")
if "cantidad" not in producto:
raise ValueError("Falta la cantidad")
if producto["precio"] < 0:
raise ValueError("El precio no puede ser negativo")
if producto["cantidad"] <= 0:
raise ValueError("La cantidad debe ser positiva")
Luego el cálculo puede usar esa validación:
def calcular_subtotal(productos):
subtotal = 0
for producto in productos:
validar_producto(producto)
subtotal += producto["precio"] * producto["cantidad"]
return subtotal
Si una función debe lanzar una excepción ante datos inválidos, conviene probarlo.
import pytest
from ventas import calcular_subtotal
def test_calcular_subtotal_falla_si_falta_precio():
productos = [{"cantidad": 2}]
with pytest.raises(ValueError, match="Falta el precio"):
calcular_subtotal(productos)
La prueba documenta el comportamiento esperado ante un error.
Un buen mensaje de error debe ayudar a corregir el problema. Evita mensajes genéricos como "error" o "datos inválidos" si puedes ser más específico.
raise ValueError("La cantidad debe ser positiva")
Si el contexto importa, agrégalo con cuidado:
raise ValueError(f"La cantidad debe ser positiva: {producto['cantidad']}")
Mejora el manejo de errores de esta función:
def calcular_promedio(datos):
try:
total = 0
for item in datos:
total += item["valor"]
return total / len(datos)
except Exception:
return 0
Problemas:
0 para errores muy distintos.def obtener_valor(item):
if "valor" not in item:
raise ValueError("Falta la clave valor")
return float(item["valor"])
def calcular_promedio(datos):
if not datos:
raise ValueError("No se puede calcular promedio sin datos")
valores = [obtener_valor(item) for item in datos]
return sum(valores) / len(valores)
Ahora los errores son explícitos. Quien llama a la función puede decidir si muestra un mensaje, registra el problema o detiene el proceso.
En ventas_demo, agrega validación para productos y realiza estas tareas:
precio o sin cantidad.pytest.raises.python -m ruff check src tests
python -m pytest
Antes de continuar, verifica que puedes hacer lo siguiente:
except Exception cuando no corresponde.try cerca de la operación riesgosa.raise ... from error cuando transformas excepciones.pytest.raises.En este tema vimos que el manejo de errores también puede producir code smells. Silenciar excepciones, capturar errores demasiado generales o ubicar mal los try vuelve el sistema más difícil de depurar y mantener.
En el próximo tema estudiaremos complejidad ciclomática: cómo medirla y reducirla en funciones Python.