skip to content

How does weakref.finalize improve on __del__ for releasing an external resource?

level: seniorimportance: nice to knowfreq 18%

answer

  1. Cleanup registered from outside the class
  2. You get a handle you can call
  3. It fires at most once, ever
  4. Do not let the callback hold the object
  5. Still pending at exit? It runs

basics

~20 s

weakref.finalize attaches a callback to an object from the outside: it runs at most once when the object is collected, or at normal interpreter exit if the object is still alive, and it can be cancelled or invoked early without touching the class.

solid answer

~50 s

`weakref.finalize(obj, func, *args, **kwargs)` registers `func` to be called once when `obj` is garbage collected. The returned finalizer object is kept alive by an internal registry, so you do not have to store it; its `alive` attribute says whether the callback is still pending, calling the finalizer runs the callback immediately and returns its result, and `detach()` cancels it. By default `atexit` is true, so finalizers still pending at normal interpreter shutdown are run then, in reverse creation order. Against `__del__` the wins are concrete: the callback is not on the object's own teardown path, it cannot be lost by a subclass that forgets to call up, it is guaranteed to run at most once, and it can be triggered deterministically by the explicit close path. The one rule: `func` and its arguments must not reference `obj`, or the object can never be collected.

code

python · 15 lines
python
import weakref

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

def flush(name):
    print("flushed", name)

c = Cache("pages")
fin = weakref.finalize(c, flush, c.name)
print(fin.alive)
del c
print(fin.alive)
print(fin())

go deeper

for a junior

Know that Python offers a cleanup hook you register from outside a class, so cleanup does not have to live in a finalizer method, and that you still call an explicit close in normal code.

for a middle

Explain the API: registration returns a callable handle with an alive flag and a detach, the callback runs at most once, and the callback must not capture the object or it can never be collected.

for a senior

Demonstrate the ownership design: explicit close invokes the finalizer so automatic release becomes a backstop, and be clear that shutdown coverage still ends where normal interpreter finalization ends.

for a principal

Decide when non-memory resources deserve this pattern at all versus a context-managed or pooled ownership model, and how the convention is enforced consistently across teams and libraries.

### The shape of the API ```python import weakref class Cache: def __init__(self, name): self.name = name def flush(name): print("flushed", name) c = Cache("pages") fin = weakref.finalize(c, flush, c.name) ``` `weakref.finalize(obj, func, *args, **kwargs)` says: *when `obj` is collected, call `func(*args, **kwargs)` — once*. The call returns a finalizer object with a small, well-chosen surface: * **`alive`** — `True` while the callback is still pending. * **calling the finalizer** — `fin()` runs the callback immediately, returns its result, and marks the finalizer dead so it will not fire again. * **`detach()`** — cancels it, returning the original `(obj, func, args, kwargs)` if it was still alive. * **`peek()`** — inspects the same tuple without cancelling. * **`atexit`** — a flag, true by default, meaning a still-pending finalizer is also run at normal interpreter shutdown. The finalizer object is registered in a module-level table, so it stays alive even if you drop your reference to it. You get a handle if you want one and lose nothing if you throw it away. ### Why it beats a finalizer method **It is external.** `__del__` is part of the class, so it is inherited, overridable, and easy to break: a subclass that defines its own `__del__` without calling the parent's silently loses the cleanup. A finalizer registered from the constructor — or, better, from the factory that acquires the resource — belongs to the acquisition site instead of the type hierarchy. **It is off the deallocation path.** An object with `__del__` carries a finalizer slot for its whole life and is torn down through it. A `weakref.finalize` callback is invoked by the weak-reference machinery instead, which keeps the object's own teardown ordinary. **It runs at most once, by construction.** No resurrection subtleties to reason about: once fired or detached, the finalizer is dead. **It is callable early and cancellable.** This is the feature that makes the pattern work in real code. The explicit `close()` calls the finalizer, which performs the release and marks it dead; the automatic path is then a genuine backstop rather than a competing owner. ```python class Session: def __init__(self, handle): self._handle = handle self._finalizer = weakref.finalize(self, self._release, handle) @staticmethod def _release(handle): handle.close() def close(self): self._finalizer() @property def closed(self): return not self._finalizer.alive ``` Note carefully what `_release` receives: the raw handle, not `self`. That is the discipline the API demands. ### The trap: never capture the object If `func` or any argument holds a strong reference to `obj`, the registry keeps that reference alive, `obj` is never collected, and the finalizer never fires. This is why `_release` above is a `staticmethod` taking the handle — a bound method `self._release` would capture `self` and pin the session forever. Pass the resource, never the owner. ### Shutdown behaviour With `atexit` left at its default, still-pending finalizers run during normal interpreter shutdown, in reverse order of creation — the same last-in-first-out reasoning `atexit` handlers use. That gives one registration point covering both "the object was dropped mid-run" and "the process is ending with the object still live", which is precisely the pair that `__del__` handles unreliably and an `atexit` handler alone does not handle at all. Setting `atexit` to false opts out, for cleanup that only makes sense while the program is genuinely still running. The usual caveat survives: shutdown-time finalizers are still only reached on a *normal* exit. `os._exit`, an unhandled terminating signal or a fatal error skip them exactly as they skip everything else, so `weakref.finalize` improves reliability against programmer error, not against process death. ### How it compares with the other exit hooks A handler registered with `atexit.register` is bound to the *process*: it runs once at normal shutdown, whether or not the object it cares about is still relevant, and it holds whatever it captures alive for the whole run. A `__del__` method is bound to the *type*: it runs on every instance, on the deallocation path, and cannot be cancelled. `weakref.finalize` is bound to the *instance*, from the outside, and covers both endings — the object being collected mid-run and the process ending with the object still live. That is why it is the hook to reach for when the thing being released belongs to one object rather than to the program as a whole. ### When to reach for it Use it for a non-memory resource with a real release step — a temporary directory, an OS handle, a native allocation from an extension module, a registration in some external table — where you want an explicit close, a guarantee against double-release, and a net for callers who forget. Do not use it as a general memory-management tool: ordinary objects need no finalizer at all.

  • Why must the callback and its arguments avoid referencing the object being finalized?
    The finalizer is held in a module-level registry, so anything it captures is strongly reachable for as long as the finalizer is alive. Capturing the object — directly, through a bound method, or through a closure — makes it permanently reachable, so it is never collected and the callback never fires. Pass the underlying handle, path or descriptor instead, or use a static method that takes only what the release needs.
  • How does calling the finalizer from an explicit close() prevent a double release?
    Calling it performs the callback and marks the finalizer dead, so the automatic path can no longer fire. The explicit close becomes the primary owner and the collection-time and shutdown-time paths become a backstop that only runs when close was never called. The `alive` attribute then doubles as an accurate closed flag without any extra bookkeeping.
  • Does weakref.finalize protect cleanup against a process killed by a signal?
    No. Its shutdown pass runs only during normal interpreter finalization, so an uncaught terminating signal, SIGKILL, `os._exit` or a fatal error skip it just as they skip every other Python-level cleanup. It hardens against forgotten close calls and dropped references, not against abrupt process death — anything that must survive that has to be written as it is produced.

saying these in an interview costs you the question

  • Passes self or a bound method as the callback
  • Stores the finalizer only in a local and expects it to die
  • Believes it survives SIGKILL or os._exit
  • Thinks it can fire twice if close is also called
  • Treats it as a general garbage-collection tuning knob

context