26.6 Object Lifecycle: Creation, __init__, and Destruction
The __init__ Method: The Object Constructor
The __init__ method is the most fundamental and frequently used special method in Python. It is not technically a constructor—the actual object creation is handled by the __new__ method—but rather an initializer. After the __new__ method has created a new instance of the class, __init__ is automatically called to initialize the new object’s attributes and put it into a valid initial state. Its purpose is to ensure that every new object starts its life with the necessary data. The first parameter of __init__ is always self, which is a reference to the newly created instance being initialized. Subsequent parameters are used to pass initial values into the object.
class DatabaseConnection:
def __init__(self, host, port, username, password):
# These assignments create and set the initial state of the object
self.host = host
self.port = port
self.username = username
self._password = password # Note the underscore, a convention for "internal"
self.is_connected = False # A default state not set by parameters
def connect(self):
# ... logic to connect using the initialized attributes
self.is_connected = True
print(f"Connected to {self.host}:{self.port}")
# Creating an object: arguments are passed to __init__
conn = DatabaseConnection("db.example.com", 5432, "admin", "secret")
print(conn.host) # Output: db.example.com
print(conn.is_connected) # Output: False
conn.connect() # Output: Connected to db.example.com:5432
The __new__ Method: The Actual Constructor
While __init__ customizes a new instance, __new__ is the method that actually creates it. It is a static method (though it doesn’t need the @staticmethod decorator) that takes the class whose instance is to be created as its first argument (cls). It is responsible for returning a new instance of the class. You rarely need to override __new__; it’s primarily used when inheriting from immutable types like str, int, or tuple, where the object’s value is set at creation and cannot be changed later in __init__.
class UpperCaseStr(str):
"""A string class that always creates an uppercase version."""
def __new__(cls, value):
# We must create and return a new str instance from the uppercase value
new_instance = super().__new__(cls, value.upper())
return new_instance
# __init__ is not needed here for the immutable object's core value,
# as it's already set by __new__.
normal_str = str("Hello")
upper_str = UpperCaseStr("Hello")
print(normal_str) # Output: Hello
print(upper_str) # Output: HELLO
The __del__ Method and Object Destruction
The __del__ method is known as a finalizer or destructor. It is called when an object is about to be destroyed, which happens when its reference count drops to zero and it is garbage collected. However, it is crucial to understand that __del__ is unpredictable. The exact time of garbage collection is non-deterministic and is handled by the Python interpreter. You should never rely on __del__ for critical cleanup tasks like closing files or network connections; use it only for non-critical cleanup. For reliable resource management, always use context managers (with statements) instead.
class ResourceLogger:
def __init__(self, name):
self.name = name
print(f"Resource {self.name} was created.")
def __del__(self):
# This will print when the object is garbage collected.
print(f"Resource {self.name} is being destroyed.")
def example_function():
logger = ResourceLogger("Temporary Logger")
print("Function is running...")
# When the function exits, 'logger' goes out of scope.
# Its reference count drops to zero, so it becomes eligible for GC.
print("Starting program.")
example_function()
print("Function has ended. GC can happen now, but timing is not guaranteed.")
# The __del__ message may appear here or at other times.
Common Pitfalls and Best Practices
A common pitfall is forgetting the self parameter in method definitions, including __init__. Since the method call automatically passes the instance as the first argument, its absence causes a TypeError. Another significant pitfall is the misuse of mutable default arguments. A default argument is evaluated only once, when the function is defined, not each time it is called. Using a mutable object (like a list or dictionary) as a default value leads to all instances sharing the same object, which is rarely the intended behavior.
# INCORRECT: Shared mutable default
class ProblematicInventory:
def __init__(self, items=[]): # This list is created once and shared
self.items = items
i1 = ProblematicInventory()
i2 = ProblematicInventory()
i1.items.append("sword")
print(i2.items) # Unexpectedly, both inventories now have a sword: ['sword']
# CORRECT: Use None as a sentinel value
class CorrectInventory:
def __init__(self, items=None):
if items is None:
self.items = [] # A new list is created for each instance
else:
self.items = items
i1 = CorrectInventory()
i2 = CorrectInventory()
i1.items.append("shield")
print(i1.items) # Output: ['shield']
print(i2.items) # Output: [] (an empty list, as expected)
Best practice dictates that __init__ should make the object ready for use, avoiding complex logic or external calls (like network operations) that might fail. Keep initialization simple and focused on setting up attributes. For resource acquisition that requires later release, pair __init__ with a explicit close() method or, better yet, implement the context manager protocol (__enter__ and __exit__).