Which hook reports an exception raised inside a __del__ method?
answer
- There is no caller to raise into
- Reported, then deliberately discarded
- The message says 'Exception ignored in'
- A third hook on sys, added in 3.8
- Never keep the object it hands you
basics
~20 ssys.unraisablehook, added in Python 3.8. A finalizer runs at an arbitrary moment with no meaningful caller, so its exception cannot propagate; CPython prints 'Exception ignored in: ...' and keeps going. Replacing the hook lets you log those instead of losing them.
solid answer
~50 sNothing catches it, because there is nowhere for it to go. `__del__` runs when the last reference drops — possibly deep inside unrelated code, possibly during garbage collection, possibly at interpreter shutdown — so raising there would inject a failure into code that has no relationship to the object. CPython therefore treats it as **unraisable**: it calls `sys.unraisablehook(args)` with an object carrying `exc_type`, `exc_value`, `exc_traceback`, `err_msg` and `object` (the thing being finalized), and the default, `sys.__unraisablehook__`, prints `Exception ignored in: <...>` to `sys.stderr` and execution continues. The same path handles failures in weakref callbacks and in `__del__` during collection. Install your own hook to get those into logs, but do not store the `object` field — that resurrects garbage. Better still, avoid `__del__`: use a context manager or `weakref.finalize` where failures can actually be reported.
code
python · 13 linesimport sys
def unraisable(args):
print("ignored:", args.exc_type.__name__, args.exc_value, file=sys.stderr)
sys.unraisablehook = unraisable
class Ledger:
def __del__(self):
raise RuntimeError("rollback failed in finalizer")
Ledger()
print("still running")go deeper
It is enough to know that an exception raised while an object is being cleaned up does not crash the program: Python prints an 'Exception ignored' note and carries on.
Explain why it cannot propagate — finalization has no meaningful caller — and name sys.unraisablehook as the replaceable reporting step, with sys.unraisablehook as the default that prints to stderr.
Show the operational angle: route the hook into your log sink, never retain the object it hands you, and argue for context managers or weakref.finalize over failure-prone logic in del.
Own the design rule — which resources may be released implicitly at all, and how a service guarantees that a cleanup failure becomes a visible, actionable event rather than a stderr line nobody reads.
### What "unraisable" means An exception needs a caller to propagate to. Finalization has none. `__del__` is invoked by the interpreter when an object's refcount reaches zero or the cyclic collector reclaims it, which can happen on any line of any function, on any thread, or during interpreter shutdown. If that exception were allowed to propagate, a `ValueError` from a forgotten close would surface in the middle of unrelated arithmetic simply because that was the statement that dropped the last reference. CPython refuses to do that, and calls the failure **unraisable**: it is reported and discarded. Since **3.8** the reporting step is a replaceable hook, `sys.unraisablehook`. It takes a single argument — an object whose fields are: * `exc_type`, `exc_value`, `exc_traceback` — the usual triple; * `err_msg` — an optional message such as the one used for a failing `__del__`, otherwise `None`; * `object` — the object whose finalization failed. The default, kept as `sys.__unraisablehook__`, writes `Exception ignored in: <...>` and the traceback to `sys.stderr`. The pristine default is worth calling from your own hook so console behaviour is unchanged. ### Which failures come through here * An exception raised inside `__del__`. * An exception raised by a `weakref.ref` callback or a `weakref.finalize` callable. * Failures in some C-level cleanup paths, and exceptions during garbage collection. What does **not** come through here is an ordinary exception in ordinary code; that is `sys.excepthook`'s job for the main thread and `threading.excepthook`'s for a worker. The unraisable hook is specifically the "could not propagate" channel. ### Why this matters in a real system The pattern that bites is cleanup logic hidden in a finalizer. A reconciliation job wraps a ledger handle in an object whose `__del__` issues a rollback for any batch left open. On the day a partial-failure rollback itself errors — the connection is already gone, so the call raises — the exception is unraisable. It prints one line to stderr that nothing greps for, the job exits 0, and the batch stays half-applied. Wiring `sys.unraisablehook` into the same sink as every other error is what turns that line into an alert. The deeper lesson is that `__del__` is a poor place for logic that can fail. It runs at an unpredictable time, its failures cannot be raised, it can be skipped entirely (a process killed, a cycle with a resurrected reference, an interpreter that shuts down before collecting), and during shutdown the module globals it references may already be `None`, producing bizarre `AttributeError` and `NameError` reports from code that was correct all day. Prefer an explicit `close()` exposed through `__enter__`/`__exit__`, and use `weakref.finalize` as the safety net — it runs earlier and more predictably than `__del__` and does not resurrect the object. ### Two rules for writing the hook **Do not retain what you are given.** The args object holds the dying `object`; storing it anywhere — a list of "recent errors", a closure, a cache — makes the object reachable again, which is object resurrection: the interpreter has already run its finalizer, and it will not run again. Retaining the exception value has the same effect indirectly, because a traceback pins its frames and their locals. Log the formatted text and drop the reference. **Assume a hostile environment.** The hook may run during interpreter shutdown, when `sys.stderr` is closed, when your logging handlers are torn down, or on an arbitrary thread. Keep the body minimal and defensive; an exception raised *inside* the unraisable hook is itself reported through the default path, which is noisy and unhelpful. ### Interview signal The useful answer is not just the name of the hook. It is knowing *why* the category exists — no caller to raise into — and what the consequence is: these failures are invisible unless someone routes them, and they are exactly the failures that leave state half-written. A candidate who says "exceptions in `__del__` are ignored" is half right; the interpreter ignores them for control flow but still reports them, and the report is yours to capture.
- Why shouldn't a custom unraisable hook store the object it is handed?Storing it makes an already-finalized object reachable again — resurrection. Its finalizer has run and will not run again, so it is now a live object in a cleaned-up state, and it stops being collected at all while your list holds it. Retaining the exception value has the same effect through the traceback, which pins frames and their locals. Log the text; drop the reference.
- What is a safer alternative to putting cleanup logic in __del__?Make cleanup explicit: a `close()` method exposed through `__enter__`/`__exit__`, so failures propagate to the caller who can log or retry them. Where a safety net is still wanted, `weakref.finalize` runs a callable when the object becomes unreachable, without giving that callable a reference to the object, and it can be invoked deterministically.
- Does this hook also cover exceptions raised in a weakref callback?Yes. A weakref callback runs at collection time with no caller either, so its exception takes the same unraisable path and reaches `sys.unraisablehook`. So do some C-level cleanup failures and exceptions raised during garbage collection, which is why routing this hook to your logs covers a whole family of otherwise-invisible errors.
It is a note left on a desk after everyone has gone home: nobody is there to react, so the interpreter writes it down and moves on.
saying these in an interview costs you the question
- Says exceptions in __del__ propagate to the calling code
- Thinks sys.excepthook receives finalizer failures
- Claims the failure is discarded with no report at all
- Stores the finalized object from the hook, resurrecting it
- Puts failure-prone cleanup in __del__ and calls it deterministic