Las herramientas de terminal y los servicios a menudo necesitan guardar estado, cerrar reportes, eliminar archivos temporales o registrar métricas cuando terminan. El módulo atexit en Python permite registrar funciones que se ejecutan automáticamente durante el cierre normal del intérprete, reduciendo el riesgo de olvidar una etapa final.
Esta guía explica register(), decorators, argumentos, orden LIFO, unregister() y manejo de excepciones. Complementa nuestros artículos sobre context managers, tempfile, tracebacks, faulthandler y weakref.
Registrar una función
atexit.register() añade un callable a la lista de cierre.
import atexit
def despedir():
print("Aplicación finalizada")
atexit.register(despedir)Cuando el módulo principal termina normalmente o se llama a sys.exit(), el handler se ejecuta.
Usarlo como decorator
Como register() devuelve la función original, puede utilizarse como decorator.
@atexit.register
def guardar_metricas():
print("Guardando métricas")Esta forma funciona directamente para funciones sin argumentos. Para parámetros, pásalos en la llamada de registro.
Pasar argumentos
def registrar_salida(nombre, estado="ok"):
print(f"{nombre}: {estado}")
atexit.register(
registrar_salida,
"procesamiento",
estado="finalizado",
)Los argumentos permanecen referenciados hasta el cierre. Evita capturar grafos enormes o recursos que puedan estar parcialmente desmontados.
Orden LIFO
Los handlers se ejecutan en orden inverso al registro. Si A, B y C se registran en ese orden, se llaman C, B y A.
atexit.register(lambda: print("registrado primero"))
atexit.register(lambda: print("registrado segundo"))
atexit.register(lambda: print("registrado tercero"))La documentación oficial de atexit explica que los módulos de bajo nivel suelen cargarse antes y limpiarse después.
Finalización normal
Los handlers se ejecutan cuando el módulo principal llega al final o sys.exit() genera SystemExit.
import sys
atexit.register(lambda: print("limpieza"))
sys.exit(0)El código de salida no impide la limpieza. Un handler no debería ocultar el status final previsto.
Casos en los que no se ejecuta
Atexit no es una garantía absoluta. Los handlers se omiten cuando:
- el proceso recibe una señal fatal no manejada por Python;
- el intérprete sufre un error interno fatal;
- se usa
os._exit(); - el sistema mata el proceso abruptamente;
- la máquina pierde energía.
Los datos críticos necesitan persistencia transaccional durante la operación, no únicamente al final.
Señales y cierre controlado
Las aplicaciones que responden a SIGTERM o SIGINT deberían instalar handlers con signal e iniciar una salida controlada.
import signal
import sys
def detener(signum, frame):
sys.exit(128 + signum)
signal.signal(signal.SIGTERM, detener)
signal.signal(signal.SIGINT, detener)El handler de señal debe ser simple. Convertir la señal en SystemExit permite recorrer el cierre normal.
Evitar os._exit()
os._exit() termina inmediatamente sin flush normal, bloques finally, handlers de atexit ni limpieza de alto nivel.
Existe para casos especiales de control de procesos. No debe ser la salida habitual de una aplicación.
Excepciones en handlers
Si un handler genera una excepción distinta de SystemExit, se imprime un traceback y los handlers restantes todavía tienen oportunidad de ejecutarse.
Después, la última excepción se vuelve a lanzar.
def fallar():
raise RuntimeError("falló la limpieza")
atexit.register(fallar)Cada etapa debe decidir qué errores puede registrar y tolerar sin bloquear otras tareas.
Handler resistente
import logging
logger = logging.getLogger(__name__)
def cerrar_reporte():
try:
generar_reporte_final()
except Exception:
logger.exception("No fue posible generar el reporte")
atexit.register(cerrar_reporte)No ocultes toda falla cuando el cierre incorrecto debe afectar el status del proceso.
Registrar varias veces
La misma función y argumentos pueden registrarse más de una vez. Cada ocurrencia se ejecuta.
def mostrar(nombre):
print(nombre)
atexit.register(mostrar, "A")
atexit.register(mostrar, "B")Los sistemas de plugins pueden duplicar registros accidentalmente. Protege la inicialización con estado explícito.
Eliminar con unregister()
atexit.unregister() elimina todas las ocurrencias de una función coincidente.
atexit.unregister(cerrar_reporte)Si no estaba registrada, no ocurre nada. La comparación utiliza igualdad y no solamente identidad.
Implicaciones de la igualdad
Los objetos callables pueden definir __eq__(). Varias instancias consideradas iguales pueden eliminarse juntas.
class Accion:
def __init__(self, nombre):
self.nombre = nombre
def __call__(self):
print(self.nombre)
def __eq__(self, otro):
return isinstance(otro, Accion) and self.nombre == otro.nombrePara handlers importantes, prefiere funciones simples o conserva referencias claras.
No modificar registros durante el cierre
Registrar o eliminar funciones mientras se ejecutan handlers tiene efectos indefinidos.
Construye la lista durante el startup. Si el orden debe ser dinámico, registra un coordinador que maneje su propia colección.
Coordinador de limpieza
acciones = []
def agregar_accion(funcion):
acciones.append(funcion)
def ejecutar_acciones():
for funcion in reversed(acciones):
try:
funcion()
except Exception:
logger.exception("Falló una acción de limpieza")
atexit.register(ejecutar_acciones)El patrón centraliza orden, logging y métricas, pero mantiene las mismas limitaciones ante cierres abruptos.
No iniciar threads
Desde Python 3.12, iniciar una thread desde un handler genera RuntimeError.
import threading
def incorrecto():
thread = threading.Thread(target=trabajo)
thread.start()El runtime puede estar liberando estados internos. Inicia y detén workers antes de llegar a la fase de atexit.
No llamar fork()
También desde Python 3.12, llamar a os.fork() en un handler genera RuntimeError.
Crear procesos mientras el intérprete se desmonta puede causar races y crashes. Finaliza tareas asíncronas antes o delega al supervisor.
Threads existentes
Atexit no reemplaza un protocolo de shutdown. La thread principal debería señalar workers, esperar join() y luego terminar.
stop_event.set()
for thread in threads:
thread.join(timeout=5)El handler puede registrar que un worker sigue vivo, pero no debe crear infraestructura nueva.
Preferir context managers
Los recursos locales deberían cerrarse con with.
with open("salida.txt", "w", encoding="utf-8") as archivo:
archivo.write("resultado")El recurso se libera al dejar de necesitarlo, incluso ante excepciones. Atexit debe ser una capa final para recursos globales.
Preferir try/finally
Cuando la aplicación controla el loop principal, try/finally hace explícito el orden.
iniciar()
try:
ejecutar_loop()
finally:
detener()Este patrón es más fácil de probar. Atexit ayuda cuando la inicialización ocurre dentro de módulos.
Archivos temporales
Los objetos de tempfile normalmente poseen context managers y limpieza automática. Usa atexit solamente como fallback para recursos globales.
No supongas que la eliminación siempre ocurrirá. Guarda temporales en una ubicación segura que pueda limpiarse en el siguiente inicio.
Persistir estado ligero
Un ejemplo clásico guarda un contador.
from pathlib import Path
import atexit
ruta = Path("contador.txt")
contador = int(ruta.read_text()) if ruta.exists() else 0
def guardar():
temporal = ruta.with_suffix(".tmp")
temporal.write_text(str(contador), encoding="utf-8")
temporal.replace(ruta)
atexit.register(guardar)La escritura temporal reduce archivos parciales, pero no garantiza ejecución durante un crash.
Logging durante el cierre
El orden y la desmontagem de módulos pueden afectar logging. Registra el handler después de configurar el logger y mantén disponibles los destinos.
Evita depender de globals que otros handlers puedan modificar. Captura referencias directas necesarias.
Subintérpretes
Desde Python 3.7, los registros realizados por extensiones C son locales al subintérprete en el que fueron creados.
Los runtimes embebidos con varios intérpretes deben instalar limpieza en el contexto correcto.
Probar handlers
No termines el proceso principal de tests. Extrae la lógica a funciones ordinarias y pruébalas directamente.
def guardar_estado():
...
atexit.register(guardar_estado)
def test_guardar_estado(tmp_path):
guardar_estado()Para integración real, inicia un subprocess y verifica archivos, salida y status después de terminar.
Test en subprocess
import subprocess
import sys
resultado = subprocess.run(
[sys.executable, "app_prueba.py"],
capture_output=True,
text=True,
)
assert resultado.returncode == 0
assert "limpieza completa" in resultado.stdoutAñade escenarios con sys.exit(), excepción no manejada y señales convertidas en cierre controlado.
Aplicaciones web
Los servidores y frameworks tienen hooks de startup y shutdown. Usa el ciclo oficial para cerrar pools, colas, clientes y workers.
Atexit puede ser fallback local, pero un orquestador puede terminar workers abruptamente. La limpieza principal pertenece al framework.
Containers y Kubernetes
Los containers reciben SIGTERM y un periodo antes de SIGKILL. PID 1 debe tratar la señal, dejar de aceptar trabajo, concluir operaciones y salir normalmente.
Atexit solo se ejecuta si ese camino controlado llega al cierre de Python antes del límite.
Errores frecuentes
- Usar atexit como única protección de datos críticos.
- Esperarlo después de
os._exit()o SIGKILL. - Iniciar thread o proceso en un handler.
- Registrar una función varias veces por accidente.
- Cambiar registros durante la limpieza.
- Depender de logging ya finalizado.
- Usar atexit en lugar de
with. - Ejecutar tareas largas al cerrar.
Buenas prácticas
- Registra handlers cortos e idempotentes.
- Usa el orden LIFO conscientemente.
- Prefiere context managers y
finally. - No inicies threads ni uses fork.
- Protege escrituras con archivos temporales.
- Prueba lógica directa y en subprocess.
- Integra señales y ciclo del framework.
- Planifica recuperación tras cierre abrupto.
Conclusión
El módulo atexit en Python registra funciones ejecutadas en orden inverso durante el cierre normal. Es útil para métricas finales, persistencia ligera y recursos globales que no tienen un mejor punto de cierre.
La garantía es limitada: señales fatales, os._exit(), crashes y pérdida de energía omiten los handlers. Con funciones cortas, idempotencia, context managers y un protocolo explícito, atexit funciona como una capa final de organización y no como sustituto de durabilidad.






