Back to Blog
Python

Python del vs Garbage Collection: The Real Difference

python del vs garbage collection: Understand the difference between Python's del statement and garbage collection, including reference counting, cyclic references, and...

Pythonmemory managementgarbage collectiondel statementCPythonreference counting
Diagram showing a Python del statement removing a reference while the garbage collector handles cyclic references.

The difference between del and garbage collection in Python often confuses developers because both appear to free memory. In CPython, del removes a name binding and decrements a reference count, while the garbage collector handles objects that are no longer reachable, including cyclic references. Understanding python del vs garbage collection requires looking at how CPython manages memory under the hood.

What del Actually Does

The del statement in Python removes a name from the local or global namespace. It does not directly call the garbage collector. Instead, it decrements the reference count of the object that the name referred to. If that 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 is implemented in 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. If an object is part of a cycle and has a __del__ method, the cyclic collector cannot determine a safe order to finalize the objects, so it places them in gc.garbage instead of freeing them. This is a known limitation in CPython.

import gc class Finalizable: def __del__(self): print("finalizing") a = Finalizable() b = Finalizable() a.other = b b.other = a del a, b gc.collect() # a and b are moved to gc.garbage, not freed

In Python 3.4 and later, PEP 442 changed the behavior so that objects with __del__ in cycles are finalized in a safe order, but they are still not collected immediately. The collector handles them, but you may see them in gc.garbage if you enable debugging.

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 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 the use of del in exception handling. If you delete a variable that holds an exception, you may inadvertently keep traceback objects alive. The traceback references the frame, which references local variables, creating a cycle. The garbage collector can handle this, but it may take longer to free memory. Using del on the exception variable after handling can help break the cycle early.

try: risky_operation() except Exception as e: handle(e) del e # break the traceback cycle

This pattern is safe and 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.

python del vs garbage collection: Practical Usage and Code E | RYUSLOG DEV