Does the `del` statement call an object's `__del__` method in CPython?
answer
- Statement about names, not memory
- Assignment has an inverse
- One reference dropped, not all
- Last reference reaching zero triggers it
- Cycles run later, shutdown maybe never
basics
~20 sNot by itself. The del statement removes a name binding and drops one reference. CPython runs the object's del finalizer only when the last reference to that object disappears, which may be later, or during cycle collection, or never.
solid answer
~40 s`del x` is a statement about *names*: it unbinds `x` from its namespace and decrements the object's reference count by one. If other references remain — another name, a list element, a closure cell, a frame captured in a traceback — the object lives on and `__del__` does not run. When the count does reach zero, CPython deallocates the object and calls `__del__` synchronously, in whichever thread dropped that last reference. Objects trapped in reference cycles are finalized later by the cyclic garbage collector at an unpredictable moment, and objects still alive when the interpreter exits may never be finalized at all. That is why `__del__` is a last-ditch net, not a destructor: deterministic cleanup belongs in a `with` block or an explicit `close()`.
code
python · 10 linesclass Handle:
def __del__(self):
print("finalizer ran")
a = Handle()
b = a
del a
print("after del a")
del b
print("after del b")go deeper
Be ready to state the split in one breath: del name unbinds a name and drops one reference, and __del__ runs only when the last reference goes. Know that other names, lists and attributes keep an object alive.
Explain the mechanics: reference counting drives the call, it happens synchronously on the thread that released the last reference, and cycles push finalization to the cyclic collector at an unpredictable time.
Show the production judgement — a finalizer is a warning net, never the release path for descriptors, sockets or locks. Be able to point at the real cause of a delayed finalizer, such as a retained traceback or a cache holding the object.
Own the policy angle: mandate deterministic cleanup through context managers across a codebase, treat reference-count timing as a CPython implementation detail you do not build on, and decide where a finalizer earns its keep as diagnostics only.
### The statement and the method are two different things Python reuses the word `del` for two unrelated mechanisms, and the interview question exists because beginners assume one triggers the other. **`del name` is a namespace operation.** It is the inverse of assignment: it removes the binding from the local, global or class namespace, so that referring to `name` afterwards raises `NameError`. The sibling forms delegate to protocol methods — `del obj.attr` calls `__delattr__`, `del d[key]` calls `__delitem__` — and none of them is guaranteed to destroy anything. Deleting a name is bookkeeping about *labels*, not about *memory*. **`__del__` is a finalizer method.** CPython calls it as part of tearing an object down, right before the object's memory is reclaimed. Nothing in the language says when that moment arrives; the language reference only promises the method is called "when the instance is about to be destroyed". ### What actually schedules the call in CPython CPython's primary memory-management strategy is reference counting. Every live binding — a name, a list slot, a dict value, an attribute of another object, a closure cell, an argument on a live frame — counts as one reference. `del name` drops exactly one: ```python import sys a = object() b = a print(sys.getrefcount(a) - 1) # 2 references (getrefcount adds a temporary one) del a # one reference left; nothing is destroyed ``` Only when the count falls to zero does the deallocation path run, and only then does CPython invoke `__del__`. The call is synchronous and inline: it happens on the thread that dropped the last reference, at the exact statement that dropped it, before the next bytecode executes. That is the property people mistake for determinism — it *is* deterministic, but only with respect to the last reference, and you rarely know where the last reference is. Common reasons the object outlives the `del` you wrote: * another name, a container, or an attribute of a cached object still points at it; * an exception traceback is still reachable, and a traceback keeps every frame — and therefore every local — alive; * the object appears in a reference cycle, so its count never reaches zero on its own. ### Cycles and shutdown: later, or never A cycle (an object referring to itself, or two objects referring to each other) is invisible to reference counting. The cyclic collector finds such groups and, since Python 3.4 (PEP 442), finalizes them properly instead of parking them in `gc.garbage`. So `__del__` still runs — but at whatever moment a collection happens to be triggered, possibly thousands of statements later, possibly during someone else's allocation. And "never" is a real outcome. Objects still alive when the interpreter shuts down are not guaranteed to be finalized; the language documentation states this explicitly. A process that ends through `os._exit`, a signal, or a hard crash skips finalizers entirely, and objects referenced only from a daemon thread stopped at shutdown may simply be abandoned. ### Why this matters more than a trivia point The practical consequence is that `__del__` must never be the only place a scarce resource is released. A file handle, a socket, a database connection or a lock held until "the object is collected" is held for an unbounded time, and under an alternative Python implementation with a tracing collector rather than reference counting it may be held far longer still — the same code that looks tidy on CPython leaks descriptors elsewhere. The idiom to reach for instead is deterministic: a `with` block, or an explicit `close()` in a `try`/`finally`. A finalizer is reasonable as a safety net that logs a warning when someone forgot to close, and unreasonable as the primary mechanism. ```python class Session: def close(self): self.open = False def __del__(self): if getattr(self, "open", False): print("warning: Session was never closed") ``` ### What to say in the room "`del x` unbinds a name and drops one reference. `__del__` runs when the *last* reference goes away — immediately if that was the last one, later if a cycle is involved, and possibly never at interpreter exit. So I use it as a warning net, and put real cleanup in a context manager."
- If `del x` does drop the last reference, when exactly does `__del__` run?Immediately and synchronously, inside the `del` statement itself, on the thread executing it. CPython's deallocation path calls the finalizer before the next bytecode runs, so any side effect of `__del__` — a print, a log line, an exception — is observable at that exact point in the program.
- Can an object's `__del__` be skipped entirely when a program ends?Yes. The language reference does not guarantee finalizers for objects still alive at interpreter exit, and `os._exit`, a fatal signal or a segfault bypass shutdown altogether. Anything that must happen before the process ends belongs in an explicit `close()`, a `try`/`finally`, or a registered exit handler — not in `__del__`.
- Why is holding a file open until `__del__` runs a portability bug rather than a style preference?Reference counting is a CPython implementation detail, not a language guarantee. On an implementation with only a tracing collector the object may not be finalized for a long time, so descriptors, sockets or locks accumulate. Code that closes explicitly, usually through a `with` block, behaves the same everywhere.
Deleting a name is like peeling one address label off a parcel: the parcel is only thrown away once no label anywhere points to it.
saying these in an interview costs you the question
- Says `del obj` immediately frees the object's memory
- Calls `__del__` a destructor that runs at scope exit like C++
- Assumes `__del__` always runs before the process exits
- Uses `__del__` as the only place a file or socket is closed
- Thinks `del` removes the object rather than one name binding
- Believes objects in reference cycles never get finalized at all