skip to content

Why prefer weakref.finalize over __del__ for releasing an object's resource?

level: middleimportance: should knowfreq 26%

answer

  1. Cleanup that lives beside the object, not on it
  2. The callback must not hold the thing it watches
  3. Fires at most once, and again at exit
  4. The method form hides its own exceptions
  5. Still a safety net, never the policy

basics

~20 s

weakref.finalize registers a callback plus its arguments separately from the object, so it runs at most once, cannot resurrect the object, and still fires at interpreter exit. del lives on the object, swallows its own exceptions, and can be skipped.

solid answer

~50 s

`weakref.finalize(obj, func, *args)` says "call `func(*args)` when `obj` is collected", and it keeps `func` and the arguments alive itself — so the callback survives even if you drop the finalizer object, and it runs at most once. It also runs on normal interpreter shutdown by default, which `__del__` is not guaranteed to do. By contrast `__del__` is a method on the object: an exception raised inside it is printed to stderr and ignored rather than propagated, it can accidentally resurrect the object by storing `self` somewhere, and it makes every instance slightly more expensive. The critical rule for `finalize` is that neither `func` nor its arguments may reference `obj` — that would be a strong reference, so the object would never be collected and the callback would never fire. Neither mechanism replaces a `with` block; both are last-resort safety nets.

code

python · 18 lines
python
import weakref


class Ledger:
    def __init__(self, name):
        self.name = name


def close_handle(name):
    print("closed", name)


ledger = Ledger("august")
f = weakref.finalize(ledger, close_handle, "august")
print(f.alive)    # True

del ledger        # prints: closed august
print(f.alive)    # False

go deeper

for a junior

Know that both mechanisms run cleanup when an object goes away, and that neither is how you close a file — a with block is. Recognising __del__ when you read it is enough here.

for a middle

Explain the mechanics side by side: where the callback lives, the at-most-once and runs-at-exit guarantees, swallowed exceptions, and the rule that the callback must not capture the object it watches.

for a senior

Show the production judgement: finalizers are idempotent safety nets behind an explicit context manager, they must be quiet on failure, and no mechanism runs if the process is killed. Be able to diagnose a finalizer that never fires.

for a principal

Take a position on lifetime policy across a codebase: where explicit ownership and context managers are mandated, where finalizers are tolerated as backstops, and why ordering requirements between finalizers signal a design that should have an explicit owner instead.

Python offers two ways to run code when an object goes away, and they differ in where the code lives. ### `__del__` — a method on the object Defining `__del__` on a class makes CPython call it as part of deallocating an instance. It has a set of properties that make it a poor first choice: * **Exceptions are swallowed.** An exception raised inside `__del__` cannot propagate — there is no caller to propagate to — so it is written to `sys.stderr` and discarded. Cleanup that fails, fails invisibly. * **It can resurrect.** `__del__` receives `self`; storing that `self` in a module-level list makes the object live again, at which point the interpreter's bookkeeping about it is already unusual. * **Shutdown is unreliable.** During interpreter teardown, module globals the method depends on may already be gone, so a `__del__` that calls a module function can fail exactly when you most wanted it to work — and there is no guarantee every object is finalized at exit at all. * **It is easy to defeat by accident.** Any lingering strong reference — a traceback held in a local, a registry, a closure — postpones it indefinitely. ### `weakref.finalize` — a callback registered beside the object ```python import weakref ledger = Ledger('august') f = weakref.finalize(ledger, close_handle, 'august') ``` The finalizer holds `close_handle` and the argument `'august'` strongly, and `ledger` only weakly. That arrangement buys several guarantees: * **At most once.** The callback is invoked a single time, whether it is triggered by collection, by interpreter exit, or by calling the finalizer object directly. * **You may discard the handle.** The module keeps registered finalizers alive, so you do not have to store `f` anywhere to keep the registration active. If you do keep it, `f.alive` tells you whether it has fired yet, and `f.detach()` cancels it and returns the stored callback and arguments. * **It runs at exit.** By default a live finalizer is invoked during normal interpreter shutdown; passing `atexit=False` opts out. * **No resurrection.** The callback never receives the object, because it never had it. * **Exceptions still do not propagate** — a callback is called from the same context as any other finalization — but at least the callback is ordinary code you can test in isolation, since it is just a function plus arguments. ### The one rule that matters **Neither the callback nor its arguments may reference the object.** This is the mistake everyone makes on first use: ```python weakref.finalize(ledger, ledger.close) # WRONG: bound method holds ledger weakref.finalize(ledger, lambda: cleanup(ledger)) # WRONG: closure holds ledger weakref.finalize(ledger, cleanup, ledger.handle) # right: only the handle ``` Both wrong forms are strong references to `ledger`, held by the finalizer itself, so `ledger` can never be collected and the callback can only ever run at interpreter exit. The correct pattern extracts the plain data the cleanup needs — a file descriptor number, a path, a connection id — and passes that. It is a useful discipline in its own right: it forces you to notice how much of the object the cleanup actually needed. ### Neither one is resource management The important framing for an interview: both mechanisms are **safety nets, not policy**. Anything with an external resource — a file, a socket, a lock, a temporary directory — should be released deterministically by a context manager, so the release happens at a point you can read in the source. A finalizer is what catches the case where a caller ignored that contract, and it should therefore be idempotent and quiet. Reaching for a finalizer *instead of* a `with` block trades a guarantee for a hope, because the moment of collection depends on reference counting, on cycles, and on whether the process exits abruptly at all. If the process is killed, no finalizer runs by any mechanism. ### Which to write Prefer `weakref.finalize`. Write `__del__` only when the cleanup genuinely needs the object's private state and the class is under your control, and even then keep the body trivial and exception-free. If you find yourself needing ordering guarantees between several finalizers, that is a sign the lifetime should be managed explicitly by an owner rather than inferred from collection.

  • What happens to an exception raised inside a __del__ method?
    It cannot propagate, because there is no caller to receive it — deallocation happens at an arbitrary point in unrelated code. CPython writes an unraisable-exception report to standard error and continues, so the failure is invisible to the surrounding program. That is why cleanup logic in `__del__` should be trivial and defensive, and why a mechanism whose callback you can test independently is preferable.
  • Why must a weakref.finalize callback avoid referencing the object it is registered on?
    The finalizer holds the callback and its arguments with strong references. If either reaches the object — a bound method, a closure over it, or passing it as an argument — then the object always has a strong referent and can never be collected, so the callback only ever runs at interpreter exit, if at all. Pass the plain data the cleanup needs instead.
  • Does registering a finalizer remove the need for a context manager?
    No. A `with` block releases the resource at a point you can read in the source; a finalizer releases it whenever collection happens to occur, which depends on reference counts, cycles and whether the process exits cleanly at all. Treat the finalizer as an idempotent safety net for callers who ignored the contract, and keep the context manager as the actual policy.

A del method is a note written inside a parcel; a registered finalizer is an instruction left with the courier — and if the note asks the courier to keep the parcel, it will never be delivered at all.

saying these in an interview costs you the question

  • Registers a bound method of the object as its own finalizer callback
  • Believes __del__ is guaranteed to run at interpreter exit
  • Expects an exception in __del__ to propagate to the caller
  • Uses finalization instead of a with block for files or sockets
  • Thinks the finalizer object must be stored to stay registered
  • Assumes a finalizer can run more than once

context