pyguides

atexit module

The atexit module lets you register functions that Python calls automatically when the interpreter shuts down cleanly. If you need something to happen regardless of how your script finishes — saving state, closing connections, printing a goodbye message — this is the module to reach for.

It is straightforward: you register a function, Python calls it on the way out. The tricky part is knowing exactly when that is and what can go wrong.

Registering a Handler

atexit.register() adds a function to the exit handler list:

import atexit

def save_state():
    with open("state.json", "w") as f:
        f.write('{"saved": true}')

atexit.register(save_state)

When the interpreter exits normally, save_state() runs. This happens even if your main code finishes by reaching the end of the script, calls sys.exit(), or exits via an unhandled exception in the main module.

register() returns the function itself, which means you can use it as a decorator:

import atexit

@atexit.register
def goodbye():
    print("Shutting down.")

The decorator form only works for functions with no required arguments.

Passing Arguments

You can pass arguments to the registered function directly through register():

def greet(name, loud=False):
    msg = f"Hello, {name}!" if not loud else f"HELLO, {name}!"
    print(msg)

atexit.register(greet, "Alice")
atexit.register(greet, "Bob", loud=True)
# At exit: prints "Hello, Alice!" then "HELLO, Bob!"

The LIFO ordering matters here. If you register modules in a specific order and want them torn down in reverse, atexit handles that automatically.

Execution Order

Exit handlers run in last in, first out order. Register A, then B, then C:

atexit.register(lambda: print("A"))
atexit.register(lambda: print("B"))
atexit.register(lambda: print("C"))
# Output at exit: C, B, A

This matches how module import order typically works — a high-level module imports lower-level ones, so the lower-level ones should be cleaned up first. atexit enforces that automatically.

Unregistering Functions

atexit.unregister(func) removes a function from the handler list:

def cleanup():
    print("cleaning up")

atexit.register(cleanup)
atexit.unregister(cleanup)  # cleanup will NOT run at exit

You do not need a matching reference — unregister() matches by equality (==), not identity. If you registered the same function three times, all three occurrences get removed.

Calling unregister() on a function that was never registered does nothing.

What Does Not Trigger atexit

The atexit module only covers clean interpreter shutdown. Several scenarios skip the registered handlers entirely:

  • Signal termination — a SIGTERM or SIGINT that Python does not catch means atexit never runs
  • os._exit() — bypasses the Python interpreter state entirely
  • Fatal internal error — a C-level crash in the Python runtime
  • Process killkill -9, task manager force-close, etc.

For signal handling, see the signal module. For guaranteed cleanup even in hard exit scenarios, consider weakref.finalize().

Exceptions in Exit Handlers

If an exit handler raises an exception, Python prints a traceback but continues running the remaining handlers. After all handlers have had a chance to execute, the first exception that was raised gets re-raised. This means the last exception to fire is the one that propagates, not necessarily the most important one.

Warning: Fork and Threads (Python 3.12+)

Starting a new thread or calling os.fork() from within a registered exit handler is a runtime error in Python 3.12+. The reason is a race condition: when the main thread cleans up Python’s internal state after fork(), the new process or thread can try to use state that has already been freed, causing crashes.

import atexit
import os

def fork_something():
    pid = os.fork()  # Raises RuntimeError in 3.12+
    if pid == 0:
        # child process
        pass

atexit.register(fork_something)  # RuntimeError when handler runs

When to Use atexit

atexit is the right tool when you want clean, automatic cleanup with no extra scaffolding:

  • Saving application state to disk on exit
  • Closing database connections or file handles
  • Printing summary statistics at the end of a long-running script
  • Releasing resources that would otherwise leak

For more precise control — like cleaning up immediately when an object is no longer referenced rather than waiting for interpreter shutdown — use weakref.finalize().

See Also

  • signal-module — signal handling, including what does NOT trigger atexit handlers
  • sys-modulesys.exit() and interpreter lifecycle
  • weakref-modulefinalize() as an object-level cleanup mechanism that runs when an object is garbage collected