Saltar al contenido

# conceptos-avanzados-en-python

Conceptos Avanzados en Python

Volver a la portada del curso
Lectura (Markdown)

Técnicas de Manejo de Errores

Sección 3 · Manejo de Errores y Excepciones

Técnicas de manejo de errores

Ya sabes lanzar y atrapar excepciones: el lector de PyShop procesa los 1000 pedidos, descarta 19 filas dañadas con FilaInvalidaError y sigue. Falta lo demás: liberar recursos pase lo que pase, ignorar a propósito un error esperado, reportar varios errores a la vez, añadir contexto y reintentar lo que falla por causas pasajeras.

Context managers: limpiar siempre

Un bloque with llama a __enter__ al entrar y a __exit__ al salir, también si hubo excepción. __exit__ recibe el tipo, el valor y el traceback; si devuelve False, la excepción sigue su camino.

class Transaccion:
    def __enter__(self):
        print("abre")
        return self

    def __exit__(self, tipo, valor, tb):
        print("cierra; error:", tipo.__name__ if tipo else None)
        return False

try:
    with Transaccion():
        Pedido.desde_fila({"id": "9", "fecha": "2026-09-01", "cliente": "C001",
                           "ciudad": "Lima", "producto": "Taza", "cantidad": "-1"}, 9)
except FilaInvalidaError as e:
    print("afuera:", e)
# abre
# cierra; error: FilaInvalidaError
# afuera: línea 9: cantidad no positiva -1

Con contextlib.contextmanager basta un generador: lo anterior al yield es la entrada y el finally es la salida. Así está escrito cronometro en la implementación de referencia:

from contextlib import contextmanager
import time

@contextmanager
def cronometro(etiqueta, registro):
    t0 = time.perf_counter()
    try:
        yield
    finally:
        registro[etiqueta] = time.perf_counter() - t0

tiempos = {}
with cronometro("lectura", tiempos):
    pedidos = list(leer_pedidos(generar_csv(), []))
print(len(pedidos), list(tiempos))   # 981 ['lectura']

Sin el try/finally, una excepción dentro del with saltaría el registro del tiempo.

finally y suppress

finally se ejecuta incluso cuando el bloque hace return:

def venta_total(texto):
    errores = []
    try:
        return round(sum(p.importe for p in leer_pedidos(texto, errores)), 2)
    finally:
        print("filas descartadas:", len(errores))

print(venta_total(generar_csv()))
# filas descartadas: 19
# 718890.0

Cuando un error es esperado y no importa, contextlib.suppress lo dice en una línea, en lugar de un try/except: pass:

from contextlib import suppress
import os

with suppress(FileNotFoundError):
    os.remove("reporte_anterior.csv")   # si no existe, no pasa nada

Varios errores a la vez: ExceptionGroup y except*

Desde Python 3.11, ExceptionGroup agrupa excepciones independientes y except* atrapa las de cada tipo. A diferencia de except, pueden ejecutarse varias cláusulas:

errores = []
pedidos = list(leer_pedidos(generar_csv(), errores))
try:
    raise ExceptionGroup("lote con errores", errores[:3] + [KeyError("ciudad")])
except* FilaInvalidaError as grupo:
    print("filas:", [e.num_linea for e in grupo.exceptions])
except* KeyError as grupo:
    print("claves:", grupo.exceptions)
# filas: [111, 138, 141]
# claves: (KeyError('ciudad'),)

Si ninguna cláusula atrapa una parte del grupo, esa parte se vuelve a lanzar.

Contexto con add_note

add_note (3.11+) añade texto a una excepción sin cambiar su tipo; el traceback lo muestra debajo del mensaje:

e = errores[0]
e.add_note("archivo: pedidos.csv")
print(e, e.__notes__)
# línea 111: cantidad no positiva -1 ['archivo: pedidos.csv']

Niveles de logging

Los niveles son DEBUG (10), INFO (20), WARNING (30), ERROR (40) y CRITICAL (50). El logger raíz filtra por defecto todo lo menor a WARNING. Dentro de un except, log.exception registra en nivel ERROR con el traceback.

import logging, sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout,
                    format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger("ventas")
log.debug("fila %d leída", 2)                # no aparece
log.info("%d pedidos válidos", len(pedidos))  # INFO ventas: 981 pedidos válidos
log.warning("%d filas descartadas", len(errores))  # WARNING ventas: 19 filas descartadas

Reintentos

Un error de red puede desaparecer al segundo intento; un dato inválido no. Reintenta solo los errores pasajeros, con un límite y una espera creciente:

fallos = iter([True, True, False])

def subir_reporte():
    if next(fallos):
        raise ConnectionError("servicio no disponible")
    return "subido"

def con_reintentos(funcion, intentos=3, espera=0.01):
    for n in range(1, intentos + 1):
        try:
            return funcion()
        except ConnectionError as e:
            if n == intentos:
                raise
            log.warning("intento %d: %s", n, e)
            time.sleep(espera)
            espera *= 2

print(con_reintentos(subir_reporte))
# WARNING ventas: intento 1: servicio no disponible
# WARNING ventas: intento 2: servicio no disponible
# subido

assert no valida datos

assert documenta lo que el programador da por cierto. Con python -O desaparece: python -O -c "assert False; print('ignorado')" imprime ignorado. Para validar datos externos, como una cantidad negativa en el CSV, lanza una excepción.

Trampas comunes

  • except Exception: pass. Oculta bugs; usa suppress con el tipo exacto.
  • __exit__ que devuelve True. Silencia toda excepción del bloque.
  • Reintentar errores de datos. Fallarán igual en cada intento.
  • basicConfig dos veces. La segunda no hace nada si ya hay handlers; usa force=True.

Cierre

Busca en tu código cada recurso que se abre y pregúntate quién lo cierra si algo falla. Si la respuesta no es un with o un finally, ahí está el siguiente arreglo.

Recursos