Chapter 2: The Object Lifecycle: Creation, Representation, and Destruction
Every object in Python has a lifecycle. It is created, it exists in memory where it can be represented in various ways, and eventually, it is destroyed. As a senior developer, having precise control over these lifecycle events is essential for building robust and predictable systems. The Python data model provides hooks for each of these stages through specific dunder methods.
This chapter explores the trinity of object existence:
Creation:
__new__and__init__Method Types: Instance, Class, and Static Methods
Representation:
__str__and__repr__Destruction:
__del__and why you should be wary of it.
Object Creation: __new__ vs. __init__
__new__ vs. __init__Most Python programmers are intimately familiar with __init__. It's the go-to method for setting the initial state of a newly created object. However, it's not the first method called.
__new__(cls, *args, **kwargs): This is a class method responsible for creating and returning a new instance of the class. It is the very first step in the instantiation process. You will rarely need to implement__new__, but it's crucial in two main scenarios:Subclassing immutable types like
str,int, ortuple. Since these types are immutable, you must customize them at the moment of creation, before__init__is called.Implementing certain advanced design patterns, like singletons or metaclasses, where you need to control the creation process itself.
__init__(self, *args, **kwargs): This is the instance initializer. It receives the newly created instance (returned by__new__) as its first argument,self. Its job is not to create the object but to initialize its state (e.g.,self.x = x).
Here is the typical flow: MyClass(10, 20) -> MyClass.__new__(MyClass, 10, 20) -> returns an instance inst -> inst.__init__(10, 20) is called.
Let's look at an example where we subclass int to create an integer that must be within a specific range. Since int is immutable, the only time we can validate its value is before the object is actually created. This makes it a perfect job for __new__.
class RangedInt(int):
"""An integer that is restricted to a specific range."""
def __new__(cls, value):
if not (0 <= value <= 100):
raise ValueError("Value must be between 0 and 100")
instance = super().__new__(cls, value)
return instance
def __init__(self, value):
# The object's value is already set by __new__.
# Any other initialization would go here.
super().__init__()
A Note on Immutability: Why Not Validate in __init__?
__init__?A crucial question arises here: why not perform this check in __init__? The answer lies in the immutability of the parent int class. An immutable object's state cannot be changed after it is created.
By the time __init__ is called, the object already exists and its value is set in stone. If we moved the validation to __init__ and tried RangedInt(200), the __new__ method would first successfully create an int object with the value 200. Only then would __init__ run and raise a ValueError. For a brief moment, an invalid RangedInt object would have existed, violating the core guarantee of our class.
By placing the check in __new__, we enforce the constraint before the immutable object is ever created, ensuring that an invalid instance of RangedInt can never come into existence.
Method Types: Instance, Class, and Static Methods
Our discussion of __new__ brings up an important distinction in method types. You'll have noticed its first argument is cls, not self. This is because __new__ is a class method. Let's clarify the three method types available in Python.
Instance Methods (the default) The most common type of method. Its first argument is always a reference to the instance itself, conventionally named
self. Instance methods can access and modify the instance's state (self.attribute).Class Methods (
@classmethod) These methods are bound to the class, not the instance. They are marked with the@classmethoddecorator, and their first argument is a reference to the class itself, conventionally namedcls. They cannot modify instance state, but they can work with class-level data or, as we saw with__new__, be used to control the creation process. They are also commonly used to provide alternative constructors.Static Methods (
@staticmethod) These methods don't operate on either the instance or the class. They are marked with the@staticmethoddecorator and have no special first argument (selforcls). They are essentially regular functions namespaced within the class for organizational purposes. They are often utility functions that are logically connected to the class but do not need to access its state.
Let's see all three in action:
Object Representation: __repr__ vs __str__
__repr__ vs __str__Controlling how your objects are represented as strings is fundamental to creating a good developer experience. An uninformative <MyObject object at 0x10eda4c10> is a sign of an incomplete class.
__repr__(self): The goal of__repr__is to be unambiguous and complete. It should return a string that, ideally, allows a developer to recreate the object.repr(obj)is intended for developers—for use in logging, debugging, and interactive shells. A goodrepris invaluable.__str__(self): The goal of__str__is to be readable.str(obj)is intended for end-users, to be displayed in a UI or as the output ofprint(). It prioritizes clarity and legibility over completeness.
Python's behavior follows a simple fallback rule: if __str__ is not implemented, Python will use the result of __repr__ for both str() and print(). Because of this, you should always implement __repr__. Only add __str__ if you need a separate, more user-friendly representation.
Object Destruction: The Perils of __del__
__del__In languages like C++, destructors are deterministic. An object is destroyed the moment it goes out of scope. Python is different. It uses a garbage collector, which means an object is destroyed only when its reference count drops to zero, and this can happen at any time.
The __del__(self) method, called a "finalizer," is Python's hook into this process. It is called right before the object is destroyed by the garbage collector. However, you should avoid using __del__ whenever possible.
The correct pattern for deterministic cleanup of resources—like closing files or database connections—is the with statement and the context management protocol (__enter__ and __exit__), which we will cover in a later chapter.
Summary
Understanding the object lifecycle is key to writing advanced, reliable Python. Use __init__ for initialization and __new__ for controlling instance creation. Differentiate between instance, class, and static methods to operate at the correct level of abstraction. Always provide a clear __repr__ for your objects. And finally, for resource management, always prefer explicit patterns like context managers over the unpredictable nature of __del__.
Last updated