__del__ and Finalizers — Why You Can't Rely on Them
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.
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?
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?")
The Output
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
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.