When Python's Garbage Collector Gives Up
You add a __del__ method to clean up resources. Tests pass. Code ships. Then production memory climbs to 8GB over three days and you're debugging why objects that should be dead are still holding file handles.
The culprit? Cyclic references combined with custom destructors. Python's garbage collector can detect cycles, but it refuses to break them when __del__ is involved. The objects sit in memory forever, leaked but not forgotten.
Here's the pattern that bites everyone once:
import sys
class Connection:
def __init__(self, name):
self.name = name
self.logger = Logger(self) # Logger holds reference back to Connection
def __del__(self):
print(f"Closing {self.name}")
class Logger:
def __init__(self, connection):
self.connection = connection # Cyclic reference created
conn = Connection("db-primary")
del conn
print(f"Objects in gc: {len([obj for obj in gc.get_objects() if isinstance(obj, Connection)])}")
You'd expect "Closing db-primary" to print immediately. It doesn't. The Connection object leaks because Python sees the cycle ($\text{Connection} \rightarrow \text{Logger} \rightarrow \text{Connection}$) and thinks: "If I break this cycle by deleting one object first, its __del__ might access the other object that I haven't deleted yet. Undefined behavior. I'm not touching this."
And so the garbage collector just... doesn't collect it.
Continue reading the full article on TildAlice
Top comments (0)