What does atexit.register guarantee about when and in what order your callback runs?
answer
- Cleanup queued for the way out
- Order is a stack, not a queue
- Hard exits never look at the list
- register returns the function it took
- unregister removes every registration
basics
~20 satexit.register queues a callable to run during normal interpreter shutdown, and the queued callables run last-registered-first. Nothing runs after a hard exit: os._exit, an unhandled fatal signal or an interpreter crash skips every registered handler.
solid answer
~40 s`atexit.register(func, *args, **kwargs)` appends a callable to an interpreter-wide list and returns `func`, so it doubles as a decorator; `atexit.unregister(func)` removes every registration of that callable. During normal shutdown CPython walks the list in reverse registration order — LIFO — so something registered late by a module imported late is torn down before something registered early, which is usually the order dependencies were built in. The guarantee covers only *normal* termination: the main module finishing, or `sys.exit`. `os._exit`, a signal such as SIGTERM with no Python handler installed, SIGKILL, or a fatal interpreter error end the process without touching the list. On 3.14 an exception inside a handler is reported through `sys.unraisablehook`, the remaining handlers still run, and the process exit status is unaffected.
code
python · 13 linesimport atexit
@atexit.register
def first():
print("registered first, runs last")
def second():
print("registered second, runs first")
atexit.register(second)
atexit.register(print, "queued then removed")
atexit.unregister(print)
print("main body done")go deeper
Be ready to state that registered callbacks run only on normal shutdown and in reverse registration order, and to name one ending that skips them entirely, such as a process killed by a signal.
Explain the mechanics: registration appends an entry with its own arguments, the function is returned so it works as a decorator, unregister removes every copy, and the interpreter walks the list LIFO during finalization.
Show the operational judgment: shutdown callbacks are best effort, so anything that must survive is written incrementally, and a handler that blocks converts a clean stop into a hang that operators end with an uncatchable signal.
Own the policy: decide where in a service's lifecycle cleanup belongs, whether shutdown work should be explicit and observable rather than hidden in a module import, and what the system does when cleanup never runs at all.
### What the module actually is `atexit` is a very small standard-library module whose entire job is to hold a list of callables that CPython invokes while it is finalizing the interpreter. `atexit.register(func, *args, **kwargs)` appends an entry — the function plus the positional and keyword arguments it should be called with — and returns `func` unchanged, which is why it reads naturally as a decorator: ```python import atexit @atexit.register def write_checkpoint(): ... ``` `atexit.unregister(func)` removes *every* entry whose callable is `func`; registering the same function three times and unregistering once removes all three, and unregistering something that was never registered is silently fine. ### The order: last in, first out Handlers run in reverse order of registration. The reasoning is the same as for a stack of nested resources: whatever was set up last usually depends on what was set up first, so it must be torn down first. In practice registration order tends to follow import order, so a late-imported subsystem's flush runs before the logging shutdown that it wants to log through. ```python import atexit atexit.register(print, "runs second") atexit.register(print, "runs first") ``` That ordering is a real guarantee, not an implementation accident, and interviewers ask about it because the naive answer — "in the order I registered them" — is the wrong half of a coin flip. ### The much weaker guarantee: only *normal* termination The module's own docstring says it plainly: the functions run "upon normal program termination". Normal means the main module finishes, or `sys.exit` unwinds the stack in the usual way. Every abnormal ending bypasses the list entirely: * **`os._exit(status)`** calls the C `_exit` directly. The process disappears without unwinding, without flushing buffered stdio, and without running a single handler. That is not a bug — it is exactly why `os._exit` exists, for instance in a forked child that has inherited the parent's handler list and must not run the parent's cleanups a second time. * **A signal with no Python-level handler.** A default SIGTERM or SIGHUP terminates the process at the OS level. Only SIGINT has a default Python handler (it raises `KeyboardInterrupt`, which unwinds normally and therefore *does* reach the handlers). * **SIGKILL**, which cannot be caught by anything, ever. * **A fatal interpreter error** — a segmentation fault in a C extension, an unrecoverable memory error, or `Py_FatalError`. The practical consequence is a design rule rather than a trivia point: an `atexit` handler is a *best-effort* convenience, never a durability mechanism. State that must survive the process must be written as it is produced, or written to a place that can be repaired on the next start. If losing what the handler would have written is unacceptable, the handler is the wrong tool. ### What happens when a handler itself fails On CPython 3.14, an exception raised inside a handler is routed to `sys.unraisablehook`. The default hook prints a traceback tagged as an ignored exception in an atexit callback; the remaining handlers still run; and the exit status of the process is unaffected. So a handler cannot be used to turn a successful run into a failed one — if the caller must learn that shutdown work failed, the code has to detect and report the failure before shutdown, or write a marker the next start can read. ### Where handlers sit relative to object finalization Handlers run early in interpreter finalization, before the bulk of module and object teardown. An object whose `__del__` would also print something will typically be finalized *after* the handlers, not before — so a handler that reaches for a resource "the object will close later" still sees it open, while an object finalizer that reaches for something a handler already tore down sees the wreckage. ### Practical shape of a good handler Keep it short, keep it non-blocking, and make it idempotent. A handler that waits on a work queue or joins on something still in flight turns a clean shutdown into a hang, and the operator's response to a hang is a signal that skips the handler completely. Anything long-running belongs in an explicit shutdown call the program makes on its own terms; `atexit` is the safety net for the paths that forgot to make it.
- If the same function is registered three times, how many times does it run, and what does one unregister call do?It runs three times — each `atexit.register` call appends its own entry, complete with its own arguments. A single `atexit.unregister(func)` removes every entry whose callable is that function, so all three disappear at once. Unregistering a function that was never registered is a no-op rather than an error, which makes teardown code that is unsure of its own state easy to write.
- At shutdown, does an object's __del__ run before or after the atexit handlers?Handlers run first. `atexit` callbacks are invoked early in interpreter finalization, and the bulk of object and module teardown — including `__del__` on objects still alive — happens afterwards. So a handler can still safely touch objects it references, but a finalizer must not assume a handler has not already closed the resource underneath it.
- Why does a forked child process often call os._exit instead of returning normally?The child inherits the parent's registered handler list and its buffered output. Returning normally would run the parent's cleanups a second time — flushing the same buffers, deleting the same temp files, closing the same shared handles — from a process that never owned them. `os._exit` ends the child without unwinding, without flushing, and without running any inherited handler.
saying these in an interview costs you the question
- Says handlers run in registration order
- Believes atexit survives SIGKILL
- Thinks os._exit still flushes buffers and runs handlers
- Relies on a failing handler to set a nonzero exit status
- Puts blocking, long-running work in a shutdown handler
- Treats atexit as a durability guarantee for unsaved data