Python atexit — Core Concepts

What atexit does

The atexit module lets you register functions that Python calls automatically during normal interpreter shutdown. It’s the standard way to ensure cleanup happens regardless of where your program exits.

Registration

atexit.register(func, *args, **kwargs)

Register a function with optional arguments. It returns the function unchanged, so it works as a decorator too:

import atexit

# Direct registration
def cleanup_temp_files():
    print("Removing temp files...")

atexit.register(cleanup_temp_files)

# With arguments
atexit.register(print, "Server shutting down", flush=True)

# As a decorator
@atexit.register
def close_connections():
    print("Closing database connections...")

atexit.unregister(func)

Removes all instances of a function from the registry. Silently does nothing if the function wasn’t registered:

atexit.unregister(cleanup_temp_files)

Note: unregister matches by function identity (is), not equality. If you registered a lambda, you need the exact same lambda object to unregister it.

Execution order

Registered functions execute in LIFO order (last registered, first called) — like a stack:

import atexit

atexit.register(print, "first registered")
atexit.register(print, "second registered")
atexit.register(print, "third registered")

# At shutdown prints:
# third registered
# second registered
# first registered

This LIFO order is deliberate — resources acquired later often depend on resources acquired earlier, so they should be released first.

When atexit fires (and when it doesn’t)

✅ Fires on:

  • Normal program completion (script reaches the end)
  • sys.exit() calls
  • Unhandled exceptions (after the traceback prints)
  • Keyboard interrupt (Ctrl+C / KeyboardInterrupt)

❌ Does NOT fire on:

  • os._exit() — immediately terminates the process, bypassing all cleanup
  • Fatal signals like SIGKILL (kill -9) — the OS kills the process instantly
  • Catastrophic interpreter crashes
  • os.fork() child processes (atexit handlers from the parent aren’t inherited)

Exception handling

If an atexit function raises an exception, Python prints the traceback to stderr but continues calling the remaining registered functions. Exceptions don’t stop the cleanup chain:

import atexit

def will_fail():
    raise ValueError("cleanup error!")

def will_succeed():
    print("This still runs")

atexit.register(will_succeed)
atexit.register(will_fail)

# At shutdown:
# Traceback printed for will_fail
# "This still runs" printed by will_succeed

Common use cases

Saving state on exit

import atexit, json

state = {"last_run": None, "count": 0}

def save_state():
    with open("state.json", "w") as f:
        json.dump(state, f)

atexit.register(save_state)

Cleaning up temporary resources

import atexit, os

temp_files = []

def cleanup():
    for path in temp_files:
        try:
            os.unlink(path)
        except OSError:
            pass

atexit.register(cleanup)

Graceful server shutdown

import atexit

def shutdown_server(server):
    server.stop()
    print("Server stopped gracefully")

atexit.register(shutdown_server, server)

Common misconception

atexit isn’t a substitute for proper resource management. Context managers (with statements) and try/finally blocks are better for most cleanup because they run immediately when a block exits, not at program end. atexit is for program-level cleanup — things that should happen once, at the very end.

# Prefer this for file handling:
with open("data.txt") as f:
    process(f)

# Use atexit for program-level concerns:
atexit.register(save_final_metrics)

One thing to remember

atexit is for program-level cleanup that must happen at shutdown — register functions in order of dependency (most fundamental first), and know that os._exit() and kill signals bypass it entirely.

pythonstandard-librarylifecycle

See Also

  • Python Bisect Sorted Lists How Python's bisect module finds things in sorted lists the way you'd find a word in a dictionary — by jumping to the middle.
  • Python Contextlib How Python's contextlib module makes the 'with' statement work for anything, not just files.
  • Python Copy Module Why copying data in Python isn't as simple as it sounds, and how the copy module prevents sneaky bugs.
  • Python Dataclass Field Metadata How Python dataclass fields can carry hidden notes — like sticky notes on a filing cabinet that tools read automatically.
  • Python Datetime Handling Why dealing with dates and times in Python is trickier than it sounds — and how the datetime module tames the chaos