What is object resurrection in a `__del__` finalizer, and can it run twice?
answer
- The finalizer still holds self
- Storing it somewhere reachable
- Deallocation abandoned, object survives
- A flag set the first time
- PEP 442 also rescued cycles
basics
~10 sResurrection is a finalizer storing self somewhere reachable, so the dying object becomes live again and its memory is never reclaimed. CPython marks the instance finalized, so del runs at most once even then.
solid answer
~40 sWhen `__del__` runs, the object still exists and `self` is a perfectly usable reference. If the finalizer stores `self` in a module-level list, a cache or a registry, the reference count rises above zero again, deallocation is abandoned and the object goes on living — that is resurrection. CPython guards against re-running the finalizer: since Python 3.4 and PEP 442 it sets a *finalized* flag on the instance the first time, so a resurrected object that later dies again is freed silently. The language reference only calls that implementation-defined, so relying on it is unwise. PEP 442 also fixed the older, worse problem: before 3.4, a reference cycle whose members defined `__del__` was uncollectable and was dumped into `gc.garbage`, leaking permanently. Today such cycles are finalized and freed normally.
code
python · 14 linessaved = []
class Exhibit:
def __init__(self, tag):
self.tag = tag
def __del__(self):
print("finalizing", self.tag)
saved.append(self)
Exhibit("catalogue-27")
print("resurrected:", saved[0].tag)
saved.clear()
print("dropped again, no second finalizer call")go deeper
Just hold on to the shape of it: inside __del__ the object still exists, so the method can store self somewhere and keep it alive. That is called resurrection, and it is a thing to recognise rather than write.
Explain the mechanics: the reference count is re-checked after the finalizer returns, deallocation is abandoned if it rose above zero, and CPython sets a finalized flag so the method is not invoked a second time.
Show you can spot the accidental version in review — a finalizer that logs or registers self retains the object forever and its cleanup never runs again — and know that PEP 442 in 3.4 ended the gc.garbage stranding of cycles with finalizers.
Own the guidance: treat at-most-once as a CPython implementation detail rather than a contract, require finalizer logic to be idempotent and self-contained, and rule out resurrection-based pooling in favour of explicit lifecycle APIs across the codebase.
### The finalizer receives a fully working object `__del__` is called *before* the object's memory is released, and it is called with `self`. There is nothing weakened or partial about that reference: attributes work, methods work, and the object can be handed to anything. So a finalizer is free to do the one thing that sounds impossible — make the dying object reachable again: ```python registry = [] class Exhibit: def __del__(self): registry.append(self) # the object is now reachable again ``` When the finalizer returns, CPython re-checks the reference count. It is no longer zero, so deallocation is abandoned and the object stays alive, now owned by `registry`. That is resurrection. ### How many times can the finalizer run? This is the part interviewers are actually probing. The language reference says only that it is implementation-dependent whether `__del__` is called a second time when a resurrected object is about to be destroyed, and that CPython calls it once. The mechanism, introduced by PEP 442 in Python 3.4, is a per-object *finalized* bit: the first time CPython invokes the finalizer of a garbage-collected instance it sets that flag, and the flag is checked before any later call. So an object that is resurrected and dropped ten times is finalized once and then freed silently. Two practical corollaries. First, cleanup written in `__del__` genuinely happens at most once, which is convenient but is a CPython promise rather than a language one. Second, an object that resurrects itself unconditionally can never be freed while it keeps re-registering — you have built an immortal object with none of the benefits of one. ### The bigger thing PEP 442 fixed Resurrection is the memorable half of PEP 442; the important half is cycles. Before Python 3.4, if two objects referred to each other and either defined `__del__`, the collector refused to finalize them — it could not decide a safe order in which to run the finalizers, since each one might touch the other after it had been torn down. Those objects were appended to `gc.garbage` and never freed, so any long-running process that formed such a cycle leaked steadily. The standard advice of the era was "never define `__del__` on anything that might take part in a cycle". PEP 442 changed the tear-down order: the collector now runs *all* the finalizers in the unreachable group first, then re-checks reachability, and only then clears and frees the objects. Finalizers therefore see a consistent world, resurrection from inside a cycle is detected, and the finalized flag prevents a second call in a later collection. On Python 3.14 a cycle whose members define `__del__` is collected like any other, and `gc.garbage` stays empty unless something truly uncollectable appears. ```python import gc class Node: def __init__(self, tag): self.tag, self.peer = tag, None def __del__(self): print("finalizing", self.tag) a, b = Node("a"), Node("b") a.peer, b.peer = b, a del a, b gc.collect() print("garbage:", gc.garbage) # both finalized, nothing stranded ``` ### Where this bites in real systems A registry-style resurrection is occasionally deliberate — pooling an expensive object instead of letting it die, for example. It is nearly always the wrong tool: the object comes back carrying whatever state its finalizer left it in, the pool grows with no eviction path, and no reader of the class expects `__del__` to be the constructor for the next lease. A pool with explicit `acquire` and `release` methods, or an explicit re-add on close, expresses the same intent in code someone can debug. The unintentional version is easier to hit: a finalizer that logs `self`, appends to a debug list, or passes `self` to a callback that stores it. The object is silently retained, memory climbs, and because the finalizer already ran it will never run again — so the "cleanup on destroy" the class advertises never happens for that instance. ### What to say in the room "Resurrection means `__del__` stored `self` somewhere reachable, so the object survives its own destruction. CPython sets a finalized flag, so the finalizer runs at most once per object even then; the language calls that implementation-defined. PEP 442 in 3.4 introduced the flag and made cycles with finalizers collectable instead of stranding them in `gc.garbage`."
- Is relying on `__del__` running exactly once portable across Python implementations?No. The language reference explicitly calls a second call implementation-dependent and only states that CPython makes one. Another implementation with a different collector may finalize a resurrected object again, so cleanup that must not repeat should be written idempotently — guard it with a flag on the instance rather than trusting the interpreter.
- What did the collector do with a cycle containing finalizers before Python 3.4?It declared the group uncollectable and appended the objects to `gc.garbage`, where they stayed forever. Nothing was finalized and nothing was freed, so a service that repeatedly built such cycles leaked. PEP 442 in 3.4 changed the order — finalize the whole group, re-check reachability, then clear — and the stranding stopped.
- Why is resurrecting an object into a pool a poor substitute for an explicit pool API?The object returns carrying whatever state its finalizer left, no reader of the class expects destruction to be the re-entry point, the pool has no eviction path, and the finalizer will never run again for that instance so its advertised cleanup silently stops happening. Explicit acquire and release methods say the same thing in debuggable code.
A finalizer that stores self is like someone signing their own release form on the way out of the door — the paperwork stops, and nobody will process them a second time.
saying these in an interview costs you the question
- Thinks `self` is unusable or half-destroyed inside `__del__`
- Says resurrection is impossible because the object is already freed
- Expects CPython to re-run `__del__` each time the object dies again
- Still claims a `__del__` on a cycle member leaks on modern Python
- Confuses resurrection with a weak reference being revived
- Treats the at-most-once rule as a language guarantee, not a CPython detail