skip to content

In CPython, what does `del x` actually do — does it free the object?

level: juniorimportance: must knowfreq 62%

answer

  1. Names and objects are separate things
  2. The statement touches a binding
  3. One decrement, not a free
  4. Deallocation happens at zero
  5. Containers still hold their reference

basics

~20 s

del x only unbinds the name x from its namespace and drops one reference to the object. CPython deallocates the object at the moment its reference count reaches zero, so if a list, attribute or another name still holds it, nothing is freed.

solid answer

~40 s

A name and an object are different things. `del x` removes the binding of x from the local, global or class namespace and decrements the object's reference count by one; using x afterwards raises `NameError` (or `UnboundLocalError` for a function local). Deallocation is a separate event: CPython calls the type's deallocator only when the count hits zero, and that is when `__del__` runs and the memory goes back to the allocator. So `del` frees an object only when it removed the last reference. If the object also sits in a list, in an instance `__dict__`, in a closure cell or in a live traceback, it stays alive. `del d['k']` and `del obj.attr` are different statements again — they call `__delitem__` and `__delattr__`, dropping the container's reference rather than a name binding.

code

python · 10 lines
python
class Noisy:
    def __del__(self):
        print("freed")

a = Noisy()
b = a
del a
print("after del a")
del b
print("after del b")

go deeper

for a junior

Be ready to say in one sentence that the statement removes a name and drops one reference, and that the object survives while anything else refers to it. Knowing the follow-up error type — NameError, or UnboundLocalError in a function — is a bonus.

for a middle

Explain the mechanics: decrement, and deallocation only at zero, at which point the deallocator runs __del__ and cascades decrefs into the object's members. Distinguish the three targets — a name, an attribute via __delattr__, an item via __delitem__.

for a senior

Show where this matters in running code: a name deleted while a cache, closure or exception traceback still holds the object frees nothing, so the fix is usually to drop the container's reference rather than a binding. Note that prompt destruction is a CPython property, not a language guarantee.

for a principal

Own the policy angle: teams that lean on del and __del__ for cleanup have made resource lifetime implicit and implementation-dependent. Push explicit ownership through context managers and lifecycle objects so correctness does not hinge on which interpreter runs the code.

## Names, objects and the counter Every CPython object lives on the heap behind a header that carries, among other things, a **reference count**: how many references currently point at it. A *name* is not the object — it is an entry in a namespace (a function's fast-locals array, a module's global dict, a class body's dict) that holds one reference. Binding `x = obj` adds a reference and increments the count; rebinding or removing that entry drops one and decrements it. `del x` is exactly the second half of that: **it removes the binding and decrements the count by one.** That is all the statement does to the object. It does not call `free()`, it does not call `__del__`, and it does not "run the garbage collector". After the statement the name is gone, so touching it raises an error — `NameError` at module or class level, `UnboundLocalError` inside a function, where the compiler has already decided the name is local and cleared its slot. ## When memory is actually released Deallocation is a consequence, not a synonym. When a decref takes the count to **zero**, CPython immediately invokes the type's deallocator, which: 1. runs `__del__` if the type defines one, 2. decrefs everything the object referred to (its attributes, its list items, its closure cells) — which can cascade into a whole tree of frees, 3. returns the memory block to the allocator. All of that happens synchronously, inside the very instruction that dropped the last reference. This is the "deterministic destruction" people mean when they say Python cleans up promptly. The corollary is the interview point: **`del` frees the object only if it removed the last reference.** ```python a = [1, 2, 3] box = {"payload": a} del a # count goes 2 -> 1: the dict still holds the list print(box["payload"]) # the list is very much alive ``` Everything that counts as a reference keeps the object up: another name, an element of a list, tuple or set, a dict value *or key*, an instance or class attribute, a default argument value, a closure cell, an argument sitting in a live call frame, a module-level cache, and the locals of any frame captured by an exception traceback. ## `del` has three different jobs The statement is overloaded, and the target decides which protocol runs: * `del x` — unbind a name in a namespace. * `del obj.attr` — call `type(obj).__delattr__`, which normally removes the entry from the instance `__dict__`. * `del seq[i]` or `del seq[2:5]` — call `type(seq).__delitem__`. The last two remove a *container's* reference, which is often precisely what a leak fix needs: the name was never the problem, the cache entry was. ## `del x` versus `x = None` Both drop one reference. The difference is the binding: after `x = None` the name still exists and now refers to `None`; after `del x` the name is gone. In a long function, rebinding a huge intermediate to `None` before the next allocation and deleting it are equally effective at releasing memory — the choice is about whether later code expects the name to exist. ## The parts `del` does not control Two honest caveats belong in the answer: * Prompt destruction at zero is **CPython's implementation**, not a promise of the language. An implementation with a tracing collector finalizes whenever it likes, so resource cleanup should ride on `with`/`try`/`finally`, never on a well-timed `del`. * If the object participates in a reference cycle, no single decref reaches zero and the object waits for the cycle collector instead — which is why `__del__` should never be treated as a destructor with guaranteed timing. One convenience worth knowing: an `except SomeError as e:` clause **deletes `e` automatically** at the end of the block (Python 3 behaviour, PEP 3110), precisely so the exception and the frames its traceback pins do not outlive the handler. That is the interpreter reaching for the same `del` you would have written by hand. ## What good use looks like `del` earns its place in three narrow situations: releasing a large intermediate inside a long loop before the next allocation, dropping the reference that a test needs gone to assert cleanup happened, and deleting a cache entry through `__delitem__`. Sprinkling `del` at the end of functions is noise — the frame's locals are released when the frame dies anyway.

  • What happens if you use the name again after deleting it?
    At module or class level the next lookup raises `NameError`. Inside a function it raises `UnboundLocalError`, because the compiler already classified the name as local and the delete simply cleared its slot. Deleting a name that was never bound raises the same errors immediately.
  • How does `del d['key']` differ from `del x`?
    `del x` unbinds a name in a namespace. `del d['key']` calls `type(d).__delitem__`, removing the mapping entry and with it the container's reference to both key and value. Only the second one can fix a cache that is holding objects alive, because there the container, not a name, is the owner.
  • Can you force an object to be destroyed at a chosen point?
    Only by making sure you drop the last reference there — delete or rebind every name, remove it from any container, and make sure no live traceback pins the frame that held it. Even then it is a CPython-specific effect. For files, sockets and locks use a context manager or `try`/`finally`, which is deterministic on every implementation.

Deleting a name is like tearing your copy of a phone number out of your address book: the person is unaffected while anyone else still has the number, and only when the last copy is gone do they truly become unreachable.

saying these in an interview costs you the question

  • Says `del` frees the object's memory unconditionally
  • Believes `del` calls `__del__` directly
  • Thinks the garbage collector must run before `del` takes effect
  • Claims `del x` also removes the object from lists holding it
  • Treats `del x` and `x = None` as identical in every respect
  • Expects `del obj.attr` to unbind a name rather than call `__delattr__`

context