Skip to main content

How to Use Destructors in Python (del method)

Unlike C++, Python manages memory automatically through garbage collection. The __del__ method exists but behaves unpredictably, making it unsuitable for most resource cleanup scenarios.

How __del__ Works​

The destructor is called when an object's reference count drops to zero:

class Resource:
def __init__(self, name):
self.name = name
print(f"{self.name}: Created")

def __del__(self):
print(f"{self.name}: Destroyed")

obj = Resource("Alpha")
print("Object in use...")
del obj
print("After deletion")

Output:

Alpha: Created
Object in use...
Alpha: Destroyed
After deletion

When __del__ Gets Called​

The timing depends on reference counting and garbage collection:

class Item:
def __init__(self, id):
self.id = id
print(f"Item {id} created")

def __del__(self):
print(f"Item {self.id} destroyed")

def create_items():
a = Item(1)
b = Item(2)
return b # Only b survives

result = create_items()
print("Function returned")
# Item 1 destroyed when function exits (out of scope)

result = None # Now Item 2's refcount hits zero
print("Program ending")

Output:

Item 1 created
Item 2 created
Item 1 destroyed
Function returned
Item 2 destroyed
Program ending

The Circular Reference Problem​

When objects reference each other, reference counts never reach zero:

class Node:
def __init__(self, name):
self.name = name
self.neighbor = None

def __del__(self):
print(f"{self.name} deleted")

# Create circular reference
a = Node("A")
b = Node("B")
a.neighbor = b
b.neighbor = a

# Delete references
del a
del b
print("Objects deleted from namespace")

# __del__ is NOT called immediately!
# The cyclic garbage collector will eventually clean up,
# but timing is unpredictable

Output:

Objects deleted from namespace
A deleted
B deleted
warning

Circular references prevent immediate cleanup. The cyclic garbage collector handles them eventually, but you cannot rely on when __del__ will execute, or if it will at all before program exit.

Problems with __del__​

Several issues make destructors unreliable:

import gc

class Unreliable:
def __init__(self, name):
self.name = name

def __del__(self):
# Problem 1: Exceptions are ignored
raise ValueError("This won't crash the program")

# Problem 2: Global variables may be None during shutdown
# print(some_global) # May fail with AttributeError

# Problem 3: Other objects may already be deleted
# self.dependency.close() # May fail

obj = Unreliable("test")
del obj
print("No exception raised!") # __del__ exceptions are silently ignored

Output:

Exception ignored in: <function Unreliable.__del__ at 0x7fd66acb5440>
Traceback (most recent call last):
File "/home/main.py", line 9, in __del__
raise ValueError("This won't crash the program")
ValueError: This won't crash the program
No exception raised!

The Better Way: Context Managers​

For deterministic resource cleanup, use the with statement:

class DatabaseConnection:
def __init__(self, host):
self.host = host
self.connection = None

def __enter__(self):
print(f"Connecting to {self.host}")
self.connection = "active"
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.host}")
self.connection = None
# Return False to propagate exceptions, True to suppress
return False

def query(self, sql):
return f"Executing: {sql}"

# Guaranteed cleanup, even if an exception occurs
with DatabaseConnection("localhost") as db:
print(db.query("SELECT * FROM users"))

Output:

Connecting to localhost
Executing: SELECT * FROM users
Closing connection to localhost

Using contextlib for Simpler Cases​

The contextlib module simplifies context manager creation:

from contextlib import contextmanager

@contextmanager
def open_resource(name):
print(f"Acquiring {name}")
resource = {"name": name, "data": []}
try:
yield resource
finally:
print(f"Releasing {name}")
resource["data"].clear()

with open_resource("cache") as r:
r["data"].append("item")
print(f"Using {r['name']}")

Output:

Acquiring cache
Using cache
Releasing cache

Weak References to Break Cycles​

Use weakref to prevent circular reference issues:

import weakref

class Parent:
def __init__(self, name):
self.name = name
self.children = []

def __del__(self):
print(f"Parent {self.name} deleted")

class Child:
def __init__(self, name, parent):
self.name = name
# Weak reference doesn't increase refcount
self._parent_ref = weakref.ref(parent)

@property
def parent(self):
return self._parent_ref()

def __del__(self):
print(f"Child {self.name} deleted")

parent = Parent("Alice")
child = Child("Bob", parent)
parent.children.append(child)

del child
del parent
# Both destructors are called because weak reference
# doesn't create a strong cycle

Output:

Parent Alice deleted
Child Bob deleted

When to Actually Use __del__​

Legitimate use cases are rare:

class TemporaryFile:
"""Cleanup temp file as last resort (defensive programming)."""

def __init__(self, path):
self.path = path
self._closed = False

def close(self):
if not self._closed:
print(f"Properly closing {self.path}")
# os.remove(self.path)
self._closed = True

def __enter__(self):
return self

def __exit__(self, *args):
self.close()

def __del__(self):
# Backup cleanup if user forgot to close
if not self._closed:
print(f"Warning: {self.path} not properly closed!")
self.close()

# Preferred usage
with TemporaryFile("temp.txt") as f:
pass # Properly closed via __exit__

# If user forgets context manager
leaked = TemporaryFile("leaked.txt")
del leaked # __del__ provides backup cleanup

Output:

Properly closing temp.txt
Warning: leaked.txt not properly closed!
Properly closing leaked.txt
tip

Use __del__ only as a safety net for resources that should have been cleaned up elsewhere. Always provide an explicit close() method or context manager as the primary cleanup mechanism.

Comparison Summary​

Aspect__del__ DestructorContext Manager (with)
TriggerGarbage collector (unpredictable)Block exit (immediate)
ReliabilityLowHigh
Exception handlingSilently ignoredProperly propagated
Circular referencesProblematicNot affected
Use caseLast resort cleanupFiles, locks, connections

Best Practices​

  1. Prefer context managers for any resource that needs cleanup
  2. Implement close() method for explicit cleanup
  3. Use weakref to break reference cycles
  4. Never rely on __del__ for critical cleanup
  5. Don't access global state in __del__

Python's automatic memory management handles object lifecycle well, but resource management (files, connections, locks) requires explicit patterns like context managers for reliable cleanup.