Python del vs Garbage Collection: The Real Difference
Understand the difference between Python's del statement and garbage collection in CPython, including reference counting and how cyclic references are handled.
The difference between del and garbage collection in Python confuses many developers because both appear to free memory. In CPython, del removes a name binding and decrements a reference count; the cyclic garbage collector exists to reclaim objects that reference counting cannot handle, especially reference cycles. Here is what each mechanism actually does and when it runs.
What del Actually Does
The del statement removes a name, attribute, or container slot. When you delete a name, it removes that binding from the local or global namespace. It does not directly call the garbage collector. Instead, it decrements the reference count of the object the name referred to. If the count drops to zero, the object is deallocated immediately by CPython's memory manager.
x = [1, 2, 3] del x # removes the name 'x', decrements refcount of the list
After del x, the list object's reference count becomes zero if no other references exist, and its memory is freed right away. This is why del can feel immediate: for simple objects without cycles, memory is reclaimed synchronously.
However, del does not force the garbage collector to run. It only removes one reference. If other references exist, the object stays alive. For example:
a = [1, 2, 3] b = a del a # refcount of the list goes from 2 to 1; the list is still alive
Here b still references the list, so it remains in memory.
Reference Counting and Immediate Deallocation
CPython uses reference counting as its primary memory management mechanism. Every Python object stores a count of how many references point to it. When you assign a variable, pass an argument, or store an object in a container, that count increases. When a reference goes out of scope, is overwritten, or is deleted with del, the count decreases.
When the count reaches zero, CPython calls the object's deallocation function (tp_dealloc), which frees the memory. This is deterministic and happens immediately. For most objects, this means del can trigger deallocation without waiting for a garbage collection pass.
But reference counting has a well-known limitation: it cannot handle reference cycles. If two objects reference each other, their reference counts never reach zero, even if they are unreachable from the rest of the program.
class Node: def __init__(self): self.other = None a = Node() b = Node() a.other = b b.other = a del a del b # refcounts are now 1 each due to the cycle
After del a and del b, the two Node objects still reference each other, so their counts remain at 1. They are no longer reachable, but reference counting alone will never free them.
The Cyclic Garbage Collector
To handle cycles, CPython includes a cyclic garbage collector that runs periodically. It tracks container objects (objects that can hold references to other objects, like lists, dicts, and class instances) and looks for groups of objects that are unreachable from the root set but reference each other.
The collector's behavior is exposed through the gc module. It runs automatically when the number of allocations minus deallocations exceeds a threshold. You can inspect and adjust these thresholds with gc.get_threshold() and gc.set_threshold().
import gc print(gc.get_threshold()) # typically (700, 10, 10)
The collector works in generations. New objects are placed in generation 0. If they survive a collection, they are promoted to generation 1, and then generation 2. The thresholds control how often each generation is collected.
When the cyclic collector runs, it identifies unreachable cycles and frees them. This is why del alone is not sufficient for objects involved in cycles; the garbage collector is the mechanism that reclaims that memory.
del vs gc.collect(): Forcing Collection
The del statement and the gc.collect() function serve different purposes. del removes a reference; gc.collect() runs the cyclic collector immediately.
import gc del a del b gc.collect() # forces a full collection, including cycles
Calling gc.collect() is useful when you know you have created many cyclic objects and want to reclaim memory before a memory spike becomes a problem. However, it is not a replacement for del. If you remove all references to an object that is not part of a cycle, the memory is freed immediately without needing gc.collect(). The collector only adds value for cycles.
In practice, you rarely need to call gc.collect() explicitly. CPython's automatic collection is tuned to run frequently enough for most applications. Forcing collection can be useful in long-running processes that create many temporary cycles, such as in a server that handles requests with complex object graphs.
When to Use del and When to Rely on GC
Use del when you want to remove a name binding explicitly, especially for large objects that you know are no longer needed. This can help reduce memory usage sooner, particularly if the object is not part of a cycle. For example, in a loop that processes large files, you might del the file object after reading to release the file handle and its associated buffers.
for filename in filenames: data = read_large_file(filename) process(data) del data # free the large object before the next iteration
Rely on the garbage collector for objects that are part of cycles. You do not need to manually break cycles; the collector will find them. But be aware that the collector only runs when certain thresholds are met. If you create a large number of cycles in a short time, memory usage may grow until the next collection.
There is a common misconception that del forces the garbage collector to run. It does not. del only decrements reference counts. The garbage collector runs on its own schedule, or when you call gc.collect().
Common Misconceptions and Edge Cases
One edge case involves objects with __del__ methods. Before Python 3.4, if an object with __del__ was part of an unreachable cycle, the cyclic collector could not determine a safe order for finalization, so it left the objects in gc.garbage instead of freeing them. Since Python 3.4, PEP 442 changed this behavior: the collector now finalizes such objects in a safe order and reclaims the cycle normally. They may still appear in gc.garbage if you enable gc.DEBUG_SAVEALL for debugging.
import gc class Finalizable: def __del__(self): print("finalizing") a = Finalizable() b = Finalizable() a.other = b b.other = a del a, b gc.collect() # in Python 3.4+, the finalizers run and the cycle is normally reclaimed
Another edge case: del on a list element does not necessarily free the element's memory if other references exist. It only removes the list's reference.
items = [1, 2, 3] ref = items[1] del items[1] # removes the list's reference, but ref still points to 2
Memory Management in Production
In production, understanding the interaction between del and the garbage collector helps you avoid memory leaks and unexpected latency. The cyclic collector runs synchronously in the main thread, so a large collection can cause a noticeable pause. If your application creates many cycles, you may want to tune the thresholds or call gc.collect() during idle periods.
For example, a web server that creates a request object with many interconnected sub-objects might generate cycles. The automatic collector will eventually free them, but if the request rate is high, you might see memory usage climb before a collection runs. In such cases, you can call gc.collect() at the end of each request to keep memory usage flat, at the cost of some CPU time.
import gc def handle_request(request): # process request gc.collect() # reclaim cycles before returning
However, this is not always beneficial. Frequent gc.collect() calls can degrade performance because the collector must scan all live tracked objects. A better approach is to monitor memory usage and adjust the generation thresholds based on your application's allocation patterns.
Another production concern is exception handling. The exception object carries a traceback, and the traceback references the local frame, which can create a reference cycle that keeps frames alive. In long-running code, explicitly deleting the exception variable after handling it can help break that cycle earlier.
try: risky_operation() except Exception as e: handle(e) del e # help break the traceback cycle
This pattern can reduce memory retention in long-running applications.
Ultimately, del and garbage collection are complementary. del gives you fine-grained control over name bindings, while the garbage collector provides a safety net for cycles. Knowing which mechanism is responsible for freeing a given object helps you write predictable, memory-efficient Python code.