
Advanced Python: Customizing Object Construction with `__new__` and `__init__`
Understanding __new__ and __init__
The __new__ method is responsible for creating a new instance of a class. It is a static method that takes the class as the first argument and returns an instance of that class (or a subclass). In contrast, the __init__ method is an instance method used to initialize the newly created instance.
Here is a basic example to illustrate the flow:
class MyClass:
def __new__(cls, *args, **kwargs):
print("Creating instance")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print("Initializing instance")
self.value = value
obj = MyClass(10)Output:
Creating instance
Initializing instanceIn this example, __new__ creates the instance, and __init__ sets up its attributes.
Key Differences
| Feature | __new__ | __init__ |
|---|---|---|
| Purpose | Create the instance | Initialize the instance |
| Method type | Static method | Instance method |
| Return value | Returns the new instance | No return value |
| Calling order | Called before __init__ | Called after __new__ |
| Usage in subclassing | Can control the type of the object | Cannot change the type of the instance |
Practical Use Cases
1. Enforcing Immutable Types
When subclassing immutable types like int, str, or tuple, you must override __new__ because the instance is created at that point. Here's an example that creates a specialized string:
class UppercaseString(str):
def __new__(cls, value):
value = value.upper()
return super().__new__(cls, value)
s = UppercaseString("hello")
print(s) # HELLO2. Implementing a Singleton Pattern
You can use __new__ to ensure only one instance of a class is ever created:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
self.value = value
s1 = Singleton(10)
s2 = Singleton(20)
print(s1.value) # 10
print(s2.value) # 20
print(s1 is s2) # TrueNote that __init__ is still called every time, so the value might be overwritten if not handled carefully.
3. Factory Pattern Based on Input
You can use __new__ to return different types of objects based on input:
class ShapeFactory:
def __new__(cls, shape_type):
if shape_type == 'circle':
return Circle()
elif shape_type == 'square':
return Square()
else:
raise ValueError("Unknown shape type")
class Circle:
def draw(self):
print("Drawing a circle")
class Square:
def draw(self):
print("Drawing a square")
shape = ShapeFactory('circle')
shape.draw() # Drawing a circleBest Practices
- Prefer
__init__for initialization: Use__new__only when you need to control the creation process, such as for immutable types or singleton patterns. - Always call the parent's
__new__method: When overriding__new__, usesuper().__new__(cls)to ensure proper inheritance and object creation. - Be cautious with
__init__in__new__-based classes: Since__init__is always called after__new__, you must ensure it does not interfere with object identity or singleton logic. - Document the behavior clearly: When using
__new__, it's important to document how object creation works, especially for other developers using or subclassing your class.
