Python atexit: Run Cleanup on Exit

Published on: August 5, 2026
Reading time: 6 minutes

Command-line tools and services often need to save state, close reports, remove temporary files, or record metrics when they terminate. The Python atexit module registers functions that run automatically during normal interpreter shutdown, reducing the chance that a final cleanup step is forgotten.

This guide covers register(), decorator usage, arguments, LIFO order, unregister(), and exception handling. It complements our articles about context managers, tempfile, tracebacks, faulthandler, and weak references.

Register a cleanup function

atexit.register() adds a callable to the shutdown list.

import atexit


def goodbye():
    print("Application stopped")

atexit.register(goodbye)

When the main module finishes normally or sys.exit() is called, the handler runs.

Use register as a decorator

Because register() returns the original function, it can be used as a decorator.

@atexit.register
def save_metrics():
    print("Saving metrics")

This form is direct for functions without arguments. For parameters, pass them in the registration call.

Pass arguments

def record_exit(name, status="ok"):
    print(f"{name}: {status}")

atexit.register(
    record_exit,
    "processing",
    status="finished",
)

Arguments remain referenced until shutdown. Avoid capturing enormous object graphs or resources that may already be partially dismantled.

LIFO order

Handlers run in reverse registration order. If A, B, and C are registered in that order, execution is C, B, and A.

atexit.register(lambda: print("registered first"))
atexit.register(lambda: print("registered second"))
atexit.register(lambda: print("registered third"))

The official atexit documentation explains that lower-level modules tend to load earlier and are therefore cleaned up later.

Normal termination

Handlers execute when the main module reaches its end or sys.exit() raises SystemExit.

import sys

atexit.register(lambda: print("cleanup"))
sys.exit(0)

The selected status does not prevent cleanup. A handler should not silently obscure the intended final exit status.

Cases where handlers do not run

Atexit is not an absolute guarantee. Handlers are skipped when:

  • the process receives a fatal signal not handled by Python;
  • the interpreter encounters a fatal internal error;
  • os._exit() is used;
  • the operating system kills the process abruptly;
  • the machine loses power.

Critical data needs transactional persistence during normal operation, not only at the end.

Signals and controlled shutdown

Applications that need to respond to SIGTERM or SIGINT should install handlers with signal and initiate a controlled exit.

import signal
import sys


def stop(signum, frame):
    sys.exit(128 + signum)

signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)

The signal handler should remain simple. Converting the signal into SystemExit allows the normal shutdown path to run atexit handlers.

Avoid os._exit()

os._exit() terminates immediately without ordinary flushing, finally blocks, atexit handlers, or other high-level cleanup.

It exists for specialized process-control scenarios. It should not be the normal way to leave an application.

Exceptions in handlers

If a handler raises an exception other than SystemExit, a traceback is printed and remaining handlers still get a chance to run.

After all handlers have run, the last exception is re-raised.

def fail():
    raise RuntimeError("cleanup failed")

atexit.register(fail)

Each step should decide which failures can be logged and tolerated without blocking unrelated cleanup.

A resilient handler

import logging

logger = logging.getLogger(__name__)


def close_report():
    try:
        generate_final_report()
    except Exception:
        logger.exception("Final report could not be generated")

atexit.register(close_report)

Do not hide every error when failure should affect the process status. Distinguish diagnostics from shutdown correctness.

Registering the same function repeatedly

The same function and arguments may be registered more than once. Every occurrence is called.

def show(name):
    print(name)

atexit.register(show, "A")
atexit.register(show, "B")

Plugin systems can accidentally duplicate registrations. Protect initialization with explicit state.

Remove handlers with unregister()

atexit.unregister() removes every occurrence of a matching function.

atexit.unregister(close_report)

Calling it for an unregistered function has no effect. Matching uses equality, not only object identity.

Equality implications

Callable objects may implement __eq__(). Several instances considered equal can be removed together.

class Action:
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print(self.name)

    def __eq__(self, other):
        return isinstance(other, Action) and self.name == other.name

For important handlers, prefer simple functions or keep clear references.

Do not modify registrations during cleanup

Registering or unregistering functions while handlers are being executed has undefined effects.

Build the handler list during startup. If ordering must be dynamic, register one coordinator that manages its own list.

A cleanup coordinator

actions = []


def add_action(function):
    actions.append(function)


def run_actions():
    for function in reversed(actions):
        try:
            function()
        except Exception:
            logger.exception("Cleanup action failed")

atexit.register(run_actions)

This centralizes ordering, logging, and metrics while retaining the same abrupt-shutdown limitations.

Do not start threads

Since Python 3.12, starting a new thread from an atexit handler raises RuntimeError.

import threading


def incorrect():
    thread = threading.Thread(target=work)
    thread.start()

The runtime may already be releasing thread state. Start and stop workers before entering the atexit phase.

Do not call fork()

Also since Python 3.12, calling os.fork() in a handler raises RuntimeError.

Creating processes while the interpreter is dismantling itself can cause races and crashes. Finish asynchronous shutdown earlier or delegate it to an external supervisor.

Existing worker threads

Atexit does not replace a shutdown protocol. The main thread should signal workers, wait for join(), and only then terminate.

stop_event.set()
for thread in threads:
    thread.join(timeout=5)

An atexit handler can record that a worker remained alive, but it should not create new infrastructure.

Prefer context managers

Local resources should be closed with with.

with open("output.txt", "w", encoding="utf-8") as file:
    file.write("result")

The resource is released as soon as it is no longer needed, including during exceptions. Atexit should be a final layer for process-wide resources.

Prefer try/finally when possible

When the application controls its main loop, try/finally makes cleanup order explicit.

start()
try:
    run_loop()
finally:
    stop()

This pattern is easier to test. Atexit helps when initialization happens inside modules and no single shutdown point is available.

Temporary files

Objects from tempfile normally support context managers and automatic cleanup. Use atexit only as a fallback for process-wide temporary resources.

Do not assume deletion will always happen. Place temporary data in a secure location that can be cleaned on the next startup.

Persisting lightweight state

A classic example saves a counter.

from pathlib import Path
import atexit

path = Path("counter.txt")
counter = int(path.read_text()) if path.exists() else 0


def save():
    temporary = path.with_suffix(".tmp")
    temporary.write_text(str(counter), encoding="utf-8")
    temporary.replace(path)

atexit.register(save)

Temporary writing and replacement reduce partial files, but they do not guarantee execution during a crash.

Logging during shutdown

Handler ordering and module teardown can affect logging. Register the handler after logger configuration and keep destinations available.

Avoid relying on globals that other cleanup code may mutate. Capture direct references when registering.

Subinterpreters

Since Python 3.7, atexit registrations made by C extensions are local to the subinterpreter in which they were registered.

Embedded runtimes with multiple interpreters must install cleanup in the appropriate context.

Testing handlers

Do not terminate the main test process. Extract cleanup logic into ordinary functions and test those directly.

def save_state():
    ...

atexit.register(save_state)


def test_save_state(tmp_path):
    save_state()

For integration testing, start a subprocess and inspect files, output, and exit status after it terminates.

Subprocess integration test

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "test_app.py"],
    capture_output=True,
    text=True,
)

assert result.returncode == 0
assert "cleanup complete" in result.stdout

Add scenarios for sys.exit(), unhandled exceptions, and signals converted into controlled exits.

Web applications

Servers and frameworks provide startup and shutdown hooks. Use the official lifecycle to close pools, queues, clients, and workers.

Atexit may provide a local fallback, but an orchestrator can terminate workers abruptly. Core cleanup belongs in the framework lifecycle.

Containers and Kubernetes

Containers receive SIGTERM and have a grace period before SIGKILL. PID 1 should handle the signal, stop accepting work, finish current operations, and exit normally.

Atexit runs only when that controlled path reaches Python shutdown before the deadline.

Common mistakes

  • Using atexit as the only protection for critical data.
  • Expecting it after os._exit() or SIGKILL.
  • Starting a thread or process in a handler.
  • Registering one function repeatedly by accident.
  • Changing registrations during cleanup.
  • Depending on logging that has already shut down.
  • Using atexit instead of with.
  • Running lengthy operations during shutdown.

Best practices

  • Register short, idempotent handlers.
  • Use LIFO order deliberately.
  • Prefer context managers and finally.
  • Do not start threads or fork.
  • Protect writes with temporary files.
  • Test logic directly and through subprocesses.
  • Integrate signals and framework lifecycle.
  • Plan recovery after abrupt termination.

Conclusion

The Python atexit module registers functions that run in reverse order during normal interpreter shutdown. It is useful for final metrics, lightweight persistence, and process-wide resources that lack a better closing point.

The guarantee is limited: fatal signals, os._exit(), crashes, and power loss skip handlers. With short idempotent functions, context managers, and an explicit shutdown protocol, atexit becomes a final organizational layer rather than a substitute for durability.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Monitor with binary code representing pickle opcode analysis with Python pickletools
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pickletools: Analyze Pickles

    Learn Python pickletools to disassemble pickles, inspect opcodes, and optimize streams without executing untrusted data.

    Ler mais

    Tempo de leitura: 6 minutos
    05/08/2026
    Source code on screen representing analysis with Python tokenize
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tokenize: Analyze Source Code

    Learn Python tokenize to inspect tokens, comments, indentation, encodings, source positions, and rebuild code safely.

    Ler mais

    Tempo de leitura: 6 minutos
    05/08/2026
    Developer working on build automation and directory compilation with Python compileall
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python compileall: Compile Directories

    Learn Python compileall to compile directories, generate pyc files in parallel, filter paths, and control optimization and invalidation.

    Ler mais

    Tempo de leitura: 6 minutos
    04/08/2026
    Monitor with binary code representing pyc generation with Python py_compile
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python py_compile: Generate pyc Files

    Learn Python py_compile to generate pyc files, validate syntax, and control optimization and timestamp or hash invalidation.

    Ler mais

    Tempo de leitura: 5 minutos
    04/08/2026
    Code editor representing tab and space correction with Python tabnanny
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tabnanny: Fix Ambiguous Indentation

    Learn Python tabnanny to detect ambiguous tabs and spaces, scan projects, and prevent TabError and IndentationError.

    Ler mais

    Tempo de leitura: 6 minutos
    04/08/2026
    Source code and syntax representing lexical analysis with Python tokenize
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tokenize: Analyze Source Code

    Learn Python tokenize to inspect tokens, comments, encodings, source positions, and rebuild Python code safely.

    Ler mais

    Tempo de leitura: 5 minutos
    04/08/2026