Explore the advanced roles of __new__, __init__, and __del__ in Python. Learn when to use each, how they impact object lifecycle, and how to apply them in real-world scenarios like Singletons and immutable types.
In Python, object creation is controlled through a special set of methods: __new__
, __init__
, and __del__
. These methods give us the flexibility to customize how objects are created, initialized, and destroyed. While __init__
is the most commonly used method, the other two, __new__
and __del__
, allow for even more control, especially in advanced patterns such as singleton patterns, object pooling, and memory management.
In this article, we'll explore:
__new__
, __init__
, and __del__
are and how they work.Before diving into the examples, it's crucial to understand the basic roles and behaviors of each method:
__new__
method is responsible for creating and returning a new instance of the class. It's called first, before __init__
. If the object is immutable, __new__
has to return the fully initialized object, as the state of the object cannot be modified after creation.__new__
is usually overridden in advanced use cases, such as implementing design patterns like Singleton, Flyweight, subclassing immutable types like str
, tuple
, etc., or managing object creation efficiently like object pooling.__init__
is called immediately after the object is created by __new__
. Here, we initialize instance attributes and perform any necessary setup for the object.__init__
to initialize instance attributes, validate input, or perform some other setup. It’s the method that most developers interact with.__del__
method is called when an object’s reference count drops to zero, indicating that the object is about to be garbage collected. It is intended for cleanup tasks, such as releasing external resources like file handles, network sockets.__del__
if you need to release external resources when the object is destroyed. However, it should be used with caution due to Python's non-deterministic garbage collection system.While most developers rarely need to override __new__
, it provides powerful capabilities for advanced object creation strategies. Here are some scenarios where __new__
shines.
Python’s built-in immutable types like str
, tuple
, and frozenset
cannot have their internal state changed once they are created. Therefore, any transformation on these types must happen at the moment of creation. That’s where __new__
comes into play.
Let’s look at that example:
class UpperStr(str):
def __new__(cls, content):
# Modify the content before the instance is created
instance = super().__new__(cls, content.upper())
return instance
s = UpperStr("hello")
print(s) # Output: HELLO
In this example:
UpperStr
inherits from str
which is immutable.__new__
method is overridden to modify the string by converting it to uppercase before creating the object.str
objects are immutable, the transformation must occur during creation, not after.This approach ensures that once the object is created, it cannot be modified, preserving the immutability of the str
type.
The Singleton design pattern ensures that a class has only one instance throughout the application. To achieve this, we use __new__
to control object creation and ensure that only one instance is ever created.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
In this example:
__new__
method checks whether the class has already created an instance.super().__new__(cls)
call.The Flyweight pattern reduces memory usage by sharing objects that have the same state. Instead of creating a new instance every time, objects are reused from a shared pool.
class Flyweight:
_cache = {}
def __new__(cls, name):
if name not in cls._cache:
cls._cache[name] = super().__new__(cls)
return cls._cache[name]
# Only one instance for each unique 'name'
obj1 = Flyweight("apple")
obj2 = Flyweight("apple")
obj3 = Flyweight("banana")
print(obj1 is obj2) # Output: True (same instance)
print(obj1 is obj3) # Output: False (different instance)
In this example:
Flyweight
class caches instances in a dictionary based on the name
passed to __new__
.name
already exists, the existing instance is returned; otherwise, a new one is created.The __init__
method is essential for initializing objects. While __new__
controls object creation, __init__
is where we initialize the object's state. Here are a few advanced use cases of __init__
.
Dependency Injection (DI) is a technique where an object’s dependencies are provided (injected) to it during its initialization. This decouples the object from its dependencies, making it easier to test and modify.
class DatabaseConnection:
def connect(self):
return "Connected to the database."
class Service:
def __init__(self, db_connection):
self.db = db_connection
def perform_task(self):
return f"Task performed using {self.db.connect()}"
db = DatabaseConnection()
service = Service(db)
print(service.perform_task())
# Output: Task performed using Connected to the database.
In this example:
Service
class requires a DatabaseConnection
object to function.DatabaseConnection
inside the Service
class, it’s injected during initialization.In many applications, we need to parse a configuration file or dictionary and initialize an object with the values. This can be easily handled by overriding __init__
.
class Config:
def __init__(self, config_dict):
for k, v in config_dict.items():
setattr(self, k, v)
config_dict = {"host": "localhost", "port": 8080}
config = Config(config_dict)
print(config.host) # Output: localhost
print(config.port) # Output: 8080
In this example:
Config
class takes a dictionary and dynamically assigns each key-value pair as an attribute of the object using setattr()
.You can use __init__
to enforce type checks or validation on the input arguments. This is useful for ensuring that the object is always initialized with valid data.
class User:
def __init__(self, age):
if not isinstance(age, int):
raise TypeError("Age must be an integer.")
self.age = age
user = User(25) # Works fine
user_invalid = User("25") # Raises TypeError: Age must be an integer.
In this example:
User
class ensures that the age
argument passed to __init__
is an integer.TypeError
is raised, which prevents invalid data from being assigned to the object.The __del__
method is Python’s destructor, called when an object is about to be garbage collected. However, it comes with caveats that should be carefully considered.
Here’s a simple example of using __del__
to clean up external resources such as file handles or network sockets:
class FileWriter:
def __init__(self, path):
self.file = open(path, 'w')
def __del__(self):
print("Closing file.")
self.file.close()
In this example:
FileWriter
class opens a file in __init__
.__del__
method ensures that the file is closed when the object is garbage collected.__del__
may not always be called when you expect.__del__
may not be called.__del__
: If an exception is raised in __del__
, it will be ignored, and you may not even be aware that there was an issue.For managing resources like file handles, it's better to use context managers (with
statement) instead of relying on __del__
.
class FileWriter:
def __enter__(self):
self.file = open("file.txt", "w")
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
with FileWriter() as file:
file.write("Hello, world!")
In this example:
FileWriter
class implements the context manager protocol (__enter__
and __exit__
).with
statement ensures that the file is automatically closed when the block exits, even if an exception is raised.Metaclasses are classes that define the behavior of other classes. __new__
in metaclasses is used to customize class creation itself.
class Meta(type):
def __new__(cls, name, bases, dct):
dct['created_by'] = "Meta"
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=Meta):
pass
obj = MyClass()
print(obj.created_by) # Output: Meta
In this example:
Meta
is defined, and its __new__
method adds a new attribute created_by
to any class that uses it.MyClass
uses Meta
as its metaclass, and the created_by
attribute is automatically added to the class.Use case | Method to override | Why? |
---|---|---|
Subclassing str , tuple , etc. |
__new__ |
Immutable objects need early state setup |
Enforcing Singleton/Flyweight | __new__ |
Control over object creation and reuse |
Basic object state initialization | __init__ |
Assign values, validate input, inject deps |
Releasing external resources | __del__ (rare) |
Clean-up logic at end of object lifecycle |
Safe resource management | __enter__ /__exit__ |
Prefer over __del__ for determinism |
By mastering these special methods, you gain precise control over object lifecycles, enable more efficient memory and resource usage, and lay the foundation for clean, extensible software architecture in Python.