You wrote del payload expecting the object to vanish instantly. The object is still there — alias still points to it. del removes a name, not an object.
Think of del like returning a library book to the returns slot. You have removed your association with the book, but the book still exists on the cart until the librarian (the interpreter) processes it. If someone else still has the same book checked out (another reference), it never even reaches the cart. The object lives as long as any reference points to it.
Use del to remove references from names, attributes, items, or slices. Use context managers or explicit cleanup for external resources. Do not use __del__ as your normal correctness mechanism.
# [CURRENT - 3.10-3.14] Works on Python 3.xpayload = {"items": [1, 2, 3]}alias = payloaddel payloadprint(alias["items"]) # object is still alive through aliasThe language-level rule is reachability: an object may be reclaimed after it becomes unreachable. CPython adds a concrete strategy on top of that:
- immediate reclamation for many objects via reference counting
- periodic cycle detection via the cyclic garbage collector
# [CURRENT - 3.10-3.14] Works on Python 3.ximport gcprint(gc.get_threshold())print(gc.get_count())Those thresholds and counters are CPython runtime tuning details rather than language guarantees. They exist because pure reference counting cannot reclaim unreachable cycles.
# [CURRENT - 3.10-3.14] Works on Python 3.xa = []b = [a]a.append(b)del a, b# The cycle is unreachable, but reference counting alone cannot reclaim it.On interpreters such as PyPy, object reclamation timing can differ materially because the implementation is not centered on CPython-style immediate reference-count drops.
CPython uses two complementary memory management mechanisms, both implemented in Objects/object.c and Modules/gcmodule.c.
Reference counting. Every PyObject in CPython starts with a Py_REFCNT field. Py_INCREF() increments it; Py_DECREF() decrements it and calls _Py_Dealloc() when it reaches zero. This is why del x on a local variable typically reclaims the object instantly: the DELETE_FAST opcode removes the fast-local binding and decrements the reference count right away. The reference count for a local variable lives in the function's fast-locals array; removing the binding decrements the count immediately.
Cyclic garbage collector (Modules/gcmodule.c, Python 3.12). The GC runs periodically to find and collect unreachable object cycles that reference counting alone cannot handle. It uses a generational approach with three generations:
- generation 0: young objects, collected most frequently
- generation 1: survivors of one collection
- generation 2: long-lived objects, collected infrequently
The collection thresholds are tunable via gc.get_threshold() and default to (700, 10, 10) on CPython 3.12. That means: collect gen 0 every 700 allocations minus deallocations; collect gen 1 every 10 gen 0 collections; collect gen 2 every 10 gen 1 collections.
The GC algorithm tracks container objects (those that can participate in cycles: dict, list, set, tuple, custom class instances, etc.). Non-container objects such as strings, integers, and tuples containing no container objects are never tracked by the GC because they cannot own references to other objects that would form cycles.
During collection, the GC performs a reference-count subtraction phase to identify truly unreachable objects in a cycle, then calls the finalizer (if any) and reclaims the memory. Objects with __del__ methods still complicate cleanup because finalizers can observe partially torn-down object graphs, run at inconvenient times, or resurrect objects. Modern Python can collect many cycles with finalizers; gc.garbage is mostly for debug retention or extension types with legacy finalization hooks.
Use weakref.finalize instead of del for cleanup callbacks. The weakref.finalize object is a first-class handle that can be queried with .alive, detached with .detach(), and does not resurrect the object or create the same cyclical headaches that raw del methods do. See the weakref.finalize docs.
__del__ is a finalizer hook, not a destructor you control directly. Finalization timing can vary, interpreter shutdown can complicate it, and cycles plus finalizers are historically tricky.
weakref.finalize is usually the safer observation mechanism when you need a callback attached to object lifetime without keeping the object alive.
# [CURRENT - 3.10-3.14] Works on Python 3.ximport gcimport weakrefclass T: passobj = T()fin = weakref.finalize(obj, lambda: print("finalized"))print(fin.alive)del objgc.collect()print(fin.alive)del, context managers, gc, and weakref.finalize are stable Python 3 facilities. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
The reachability rule is language-level. Immediate destruction on last-reference drop is a CPython-specific operational characteristic, not something to build correctness on across implementations.
Version-sensitive notes:
- the default GC thresholds
(700, 10, 10)are CPython 3.12 defaults and may change Py_REF_DEBUGbuilds (debug mode) track reference counts differently- free-threaded CPython (PEP 703, Python 3.13+) uses biased reference counting and deferred reclamation for thread safety, meaning reference-count drops may not be immediate even on CPython
If cleanup affects correctness, you need deterministic cleanup:
withfor files, locks, transactions, and socketstry/finallyfor narrow manual lifetimes- explicit
close()orshutdown()APIs when the lifecycle is part of your domain
# [CURRENT - 3.10-3.14] Works on Python 3.xfrom io import StringIOdef process(line): print(line.strip().upper())with StringIO("start\nstop\n") as fp: for line in fp: process(line)Using del to "free memory now" is often cargo cult. It only helps when a reference would otherwise stay alive longer than necessary in the current scope.
Garbage collection is a memory-management mechanism, not a reliable resource-management policy. If release timing matters, make it explicit.
Use del sparingly for large temporary objects inside long-running scopes when that genuinely reduces retained memory pressure.
# [CURRENT - 3.10-3.14] Works on Python 3.xfrom pathlib import Pathdef summarize(path): raw = Path(path).read_bytes() header = raw[:128] del raw return header.hex()For finalization without ownership, prefer weakref.finalize. For handler/file cleanup patterns, pair this with the logging guidance in .