# conceptos-avanzados-en-python
Conceptos Avanzados en Python
Volver a la portada del cursoTé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; usasuppresscon el tipo exacto.__exit__que devuelveTrue. Silencia toda excepción del bloque.- Reintentar errores de datos. Fallarán igual en cada intento.
basicConfigdos veces. La segunda no hace nada si ya hay handlers; usaforce=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.