← Python Code Python's Hidden Traps
Browse Python Concepts

__del__ and Finalizers — Why You Can't Rely on Them

Mental Model

Think of '__del__' as a "best effort" cleanup crew that might show up sometime after everyone has left, if they remember. If there are circular references, it's like two people holding hands and refusing to let go, preventing the crew from ever taking them away. Context managers, on the other hand, are like a security guard who always locks up the building when everyone has left, no matter what.

Rule: Never rely on __del__ for resource cleanup; instead, use context managers or explicit close methods.

The Setup

You are writing a database connection pooling library. To make it user-friendly, you decide to automatically close network connections inside the destructor method '__del__' so that consumers do not have to worry about cleaning them up manually.

What Does This Print?

Broken code
Python
class Connection:
    def __init__(self, name):
        self.name = name
    def __del__(self):
        print(f"Closing database connection: {self.name}")

def use_connection():
    conn = Connection("Production-DB")
    # Creating a circular reference that leaks
    conn.self_reference = conn

use_connection()
print("Function finished executing. Has the connection closed?")
Predict when the connection is closed. Will it close immediately after the function exits, or will it remain open?

The Output

What actually happens
Function finished executing. Has the connection closed?

The connection did not close when the function finished. Because of the circular reference conn.self_reference = conn, the object's reference count never drops to zero, delaying cleanup indefinitely until the garbage collector runs its cyclic detection phase.

Why Python Does This

CPython relies primarily on reference counting for memory management. When an object's reference count drops to zero, it is destroyed immediately, and its __del__ method is called. However, if there is a reference cycle, the reference count stays above zero even if the object becomes unreachable from the stack. CPython's cyclic garbage collector will eventually find and break the cycle, but this occurs at unpredictable times. Furthermore, if an exception occurs inside a __del__ method, it is printed to stderr and ignored, making debugging silent failures incredibly difficult.

The Fix

Corrected pattern
Python
class Connection:
    def __init__(self, name):
        self.name = name
    def close(self):
        print(f"Closing database connection: {self.name}")
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

# Force developers to use explicit context management
with Connection("Production-DB") as conn:
    pass

Context managers or explicit 'close()' methods provide deterministic resource release. They are called at a predictable point (e.g., at the end of a 'with' block or when explicitly invoked), ensuring cleanup happens when it's needed, irrespective of Python's garbage collection cycle or the presence of circular references.

How This Fails in Real Systems

A microservice parsed video segments, writing temporary files to storage and deleting them in a __del__ destructor. Due to a circular reference in the processing object, thousands of temp files remained locked on disk, eventually completely filling the system's root partition and causing a full outage.

Key Takeaway

Never rely on __del__ for resource cleanup; instead, use context managers or explicit close methods.
Common mistake: Developers rely on '__del__' for critical resource cleanup, assuming it will always be called predictably when an object goes out of scope, but it can be delayed or skipped due to circular references or interpreter shutdown.