When does a `weakref.finalize` callback actually run in CPython?
answer
- Two triggers, whichever comes first
- One is the object's death
- The other is program shutdown
- Runs at most once, ever
- atexit defaults to True
basics
~10 sA weakref.finalize callback fires as soon as the object it was registered against is garbage collected, or at interpreter exit if that object is still alive. It runs at most once, whichever comes first.
solid answer
~50 s`weakref.finalize(obj, func, *args)` registers `func(*args)` to run **when `obj` is garbage collected** — in CPython, typically the moment its last strong reference goes away — and returns a finalizer object. If `obj` is still alive when the interpreter shuts down, the callback runs then instead, because the finalizer's `atexit` attribute defaults to `True`; those exit-time callbacks fire in reverse registration order. It runs **at most once**: whichever leg triggers first wins, and calling the finalizer object yourself runs the callback immediately and returns `func`'s result. `fin.alive` reports whether it is still pending, `fin.detach()` cancels it outright, and setting `fin.atexit = False` keeps the object-death leg while dropping the shutdown leg. Nothing runs at registration time, and you need not hold on to the finalizer yourself — `weakref` keeps it in a module-level registry until it fires or is detached.
code
python · 17 linesimport weakref
class Connection:
def __init__(self, name):
self.name = name
def close(name):
print("closed", name)
conn = Connection("primary")
fin = weakref.finalize(conn, close, conn.name)
print("alive before:", fin.alive)
del conn
print("alive after:", fin.alive)go deeper
Recall the two triggers — the object being garbage collected, and interpreter exit — and that whichever happens first wins. Be ready to say that registering a finalizer executes nothing immediately and that the callback runs at most once.
Explain the mechanics: a weak reference to the object, strong references to the callable and its arguments, a module-level registry that keeps the finalizer alive for you, and the alive / peek() / detach() / atexit surface you drive it with.
Show judgement about the shutdown leg: exit-time callbacks running newest-first, when atexit = False is right for work that is pointless or unsafe in a dying interpreter, and why no finalizer survives os._exit() or a killed process.
Own the policy question — whether the codebase treats finalizers as a safety net beneath explicitly scoped release or as the primary cleanup path, and what the second choice costs on a runtime whose collection timing is not CPython's prompt reference counting.
`weakref.finalize(obj, func, /, *args, **kwargs)` registers a **callback to be run when `obj` is destroyed**, and returns a *finalizer object* that lets you inspect, invoke or cancel that registration. Nothing is executed at registration time; the call only records the intent. **What it actually registers.** The finalizer holds a *weak* reference to `obj`, so it never keeps the object alive, plus **strong** references to `func`, `args` and `kwargs` — it must, because it has to be able to call them later. It also enters itself into a module-level registry inside `weakref`, which is why you never have to keep the returned object in a variable of your own; binding it to a name is only useful when you want to query or cancel it later. **When the callback runs.** There are three ways it can fire and one way it can be cancelled, and the first thing to happen wins, because a finalizer runs **at most once**: 1. **The referent is garbage collected.** In CPython that usually means the moment the last strong reference to `obj` goes away and its reference count reaches zero, so it feels prompt and deterministic; when `obj` is reachable only through a reference cycle it happens later, when the cycle collector runs. The object is already being torn down at that point, which is why the callback receives the arguments you captured rather than the object. 2. **The interpreter exits.** If the object is still alive when the program shuts down and the finalizer's `atexit` flag is true (the default), the callback is called during shutdown. Those exit-time callbacks run in **reverse order of registration** — newest first — mirroring the usual "unwind in the opposite order you set up" convention. 3. **You call the finalizer yourself.** `fin()` runs the callback immediately and returns whatever `func` returned. This is what lets a `close()` behave the same whether or not anyone remembers to call it: an explicit call does the work now, and the registration covers the case where nobody does. 4. **Never**, if you cancel it first. After any of those, `fin.alive` is `False` and further calls return `None`. **Inspecting and cancelling.** `fin.alive` is a boolean: true while the callback is still pending. `fin.peek()` returns the `(obj, func, args, kwargs)` tuple while the finalizer is alive and `None` afterwards — note that peeking hands you back a *strong* reference to the object, so do not stash the result. `fin.detach()` returns the same tuple and **unregisters** the finalizer, so the callback will never run. `fin.atexit` is a writable attribute: setting it to `False` says "run this when the object dies, but not merely because the program is ending". That distinction matters for cleanup that is pointless or actively unsafe at shutdown — flushing a cache the process is about to drop anyway, or writing to a connection in an interpreter that is already half torn down. **Why the exit leg behaves better than shutdown-time `__del__`.** A `__del__` body that runs during interpreter shutdown can find module globals already cleared, so a name it expected to import may be `None` by the time it looks. The `atexit` leg of a finalizer is driven from `weakref`'s own shutdown hook while the interpreter is still in a usable state, and the callable plus its arguments were captured back at registration time rather than looked up when the callback fires. That is why an exit-time finalizer is a far less surprising place to put "release the thing". **What it does not promise.** A finalizer is a best-effort net, not a transaction. If the process dies through `os._exit()`, an uncatchable signal, or a hard crash, no Python-level cleanup runs at all — a finalizer no more than any other exit handler. Timing is also implementation-dependent: the prompt "fires the instant the last name goes away" behaviour comes from CPython's reference counting, and a runtime with only a tracing collector will run the same callback at an unpredictable later moment, or not before exit at all. Cleanup that must happen at a known point still belongs in an explicitly scoped release; the finalizer is the backstop for the object that escaped that scope. One correctness note while wiring one up: because the finalizer keeps `func` and the arguments strongly, passing anything that refers back to `obj` — a bound method of `obj`, say — keeps the object reachable, and the collection leg can then never fire. Pass the plain data the cleanup needs (a descriptor, a path, a name) rather than the object. Interview-wise, the answer that lands is the two-legged one: **object death or interpreter exit, whichever comes first, exactly once** — plus knowing that `atexit = False` removes the second leg and `detach()` removes both.
- Do you have to keep a reference to the object weakref.finalize returns?No. Each finalizer registers itself in a module-level registry inside `weakref`, which holds it until the callback runs, until `detach()` unregisters it, or until the program exits. Binding it to a name is only worth doing when you want to query `alive`, call it early, flip `atexit`, or cancel it. Dropping the returned value on the floor does not disarm the cleanup.
- What happens if you call the finalizer object itself while it is still alive?The callback runs immediately and the call returns whatever `func` returned. The finalizer then reports `alive` as `False`, so neither the collection leg nor the exit leg will fire afterwards — it is genuinely at-most-once. Calling it again is harmless and returns `None`. That is what makes it safe to wire the same callback into an explicit `close()` and leave the registration as a backstop.
- Is a weakref.finalize callback guaranteed to run before the process goes away?No. It is best-effort. An orderly interpreter shutdown runs still-alive finalizers whose `atexit` is true, but `os._exit()`, an uncatchable signal such as SIGKILL, or a hard interpreter crash bypasses every Python-level cleanup path. Anything whose omission is a correctness or durability bug needs cleanup at a point you control, with the finalizer as the net for objects that escape it.
It is a standing instruction filed with a registry rather than a note in your own pocket: it is carried out when the object dies, and the registry — not you — keeps it safe until then.
saying these in an interview costs you the question
- Thinks the callback runs at registration time
- Believes you must keep the finalizer in a variable
- Says the callback can fire more than once
- Assumes the class must define __del__ for finalize to work
- Claims finalize keeps its referent alive
- Treats cleanup as guaranteed even after os._exit or SIGKILL