Python __init__ vs __new__: Object Creation Explained
When you write `obj = MyClass()` in Python, you're triggering a two-phase process that most developers never think about. First, `__new__` allocates memory and creates the raw object. Then,...
Key Insights
- Python’s object creation is a two-step process:
__new__creates the instance, then__init__initializes it—understanding this distinction is critical for advanced patterns like singletons and immutable type subclassing. __new__is a static method that must return an instance, while__init__is an instance method that must return None—violating these contracts leads to subtle bugs.- Most Python code only needs
__init__, but__new__becomes essential when subclassing immutable types, implementing factory patterns, or controlling instance creation at the metaclass level.
The Two-Step Object Creation Process
When you write obj = MyClass() in Python, you’re triggering a two-phase process that most developers never think about. First, __new__ allocates memory and creates the raw object. Then, __init__ initializes that object with instance-specific data. This separation exists for good reasons, and understanding it unlocks powerful patterns.
Let’s see this in action:
class Demo:
def __new__(cls):
print(f"__new__ called for {cls}")
instance = super().__new__(cls)
print(f"__new__ returning instance: {instance}")
return instance
def __init__(self):
print(f"__init__ called for {self}")
self.value = 42
print(f"__init__ finished, value set to {self.value}")
obj = Demo()
print(f"Final object: {obj}, value: {obj.value}")
Output:
__new__ called for <class '__main__.Demo'>
__new__ returning instance: <__main__.Demo object at 0x...>
__init__ called for <__main__.Demo object at 0x...>
__init__ finished, value set to 42
Final object: <__main__.Demo object at 0x...>, value: 42
Notice the sequence: __new__ runs first and creates the instance, then __init__ receives that instance and configures it.
Understanding new: The True Constructor
Despite common terminology, __new__ is Python’s actual constructor. It’s a static method (though you don’t need to decorate it) that receives the class as its first argument and returns a new instance.
Here’s the signature:
class MyClass:
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
return instance
The critical requirement: __new__ must return an instance. If you return None or forget the return statement, __init__ won’t be called, and you’ll get None instead of an object.
Let’s implement a singleton pattern—one of the most practical uses of __new__:
class DatabaseConnection:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
print("Creating new database connection")
cls._instance = super().__new__(cls)
else:
print("Returning existing database connection")
return cls._instance
def __init__(self, host, port):
# Only set these if not already initialized
if not hasattr(self, 'host'):
self.host = host
self.port = port
print(f"Initialized connection to {host}:{port}")
# First call creates the instance
db1 = DatabaseConnection("localhost", 5432)
# Second call returns the same instance
db2 = DatabaseConnection("remotehost", 5432)
print(f"Same instance? {db1 is db2}") # True
print(f"Host: {db1.host}") # localhost (from first initialization)
You can also use __new__ to return instances of different classes:
class ShapeFactory:
def __new__(cls, shape_type, *args):
if shape_type == "circle":
return super().__new__(Circle)
elif shape_type == "square":
return super().__new__(Square)
else:
return super().__new__(cls)
class Circle(ShapeFactory):
def __init__(self, _, radius):
self.radius = radius
class Square(ShapeFactory):
def __init__(self, _, side):
self.side = side
shape1 = ShapeFactory("circle", 5)
shape2 = ShapeFactory("square", 10)
print(type(shape1)) # <class '__main__.Circle'>
print(type(shape2)) # <class '__main__.Square'>
Understanding init: The Initializer
The __init__ method is what most Python developers work with daily. It’s an instance method that receives the already-created object as self and sets up its initial state. Crucially, it must return None.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
self.created_at = datetime.now()
# Implicit return None
person = Person("Alice", 30)
Here’s a comparison showing the relationship between both methods:
class DetailedExample:
def __new__(cls, value):
print(f"1. __new__ called with value={value}")
instance = super().__new__(cls)
print(f"2. __new__ created instance at {id(instance)}")
return instance
def __init__(self, value):
print(f"3. __init__ called on instance at {id(self)}")
self.value = value
print(f"4. __init__ set value to {self.value}")
obj = DetailedExample(100)
print(f"5. Final object at {id(obj)} with value {obj.value}")
Key Differences and When Each is Called
Here’s what you need to know:
| Aspect | __new__ |
__init__ |
|---|---|---|
| Purpose | Creates the instance | Initializes the instance |
| Method Type | Static method | Instance method |
| First Parameter | cls (the class) |
self (the instance) |
| Return Value | Must return an instance | Must return None |
| When Called | Before __init__ |
After __new__ |
| Inheritance | Must explicitly call parent | Automatically chains if not overridden |
class ExecutionFlow:
def __new__(cls, x, y):
print(f"__new__: cls={cls}, args=({x}, {y})")
instance = super().__new__(cls)
# Can set attributes here, but unusual
instance.created_in_new = True
return instance # MUST return instance
def __init__(self, x, y):
print(f"__init__: self={self}, args=({x}, {y})")
self.x = x
self.y = y
# MUST NOT return anything (or return None)
# return self # This would raise TypeError!
obj = ExecutionFlow(10, 20)
print(f"created_in_new: {obj.created_in_new}")
print(f"x: {obj.x}, y: {obj.y}")
Practical Use Cases
Subclassing Immutable Types
When you subclass immutable types like int, str, or tuple, you must use __new__ because you can’t modify the object after creation:
class PositiveInt(int):
def __new__(cls, value):
if value < 0:
raise ValueError("Value must be positive")
return super().__new__(cls, value)
# __init__ would be too late—the int value is already set
def __init__(self, value):
# This runs, but can't change the int value
self.original_input = value
num = PositiveInt(42)
print(num) # 42
print(num + 10) # 52
try:
negative = PositiveInt(-5)
except ValueError as e:
print(f"Error: {e}")
Factory Pattern with Type Selection
class DataParser:
def __new__(cls, data):
if isinstance(data, dict):
return super().__new__(JSONParser)
elif isinstance(data, str) and data.startswith('<?xml'):
return super().__new__(XMLParser)
else:
return super().__new__(PlainTextParser)
class JSONParser(DataParser):
def __init__(self, data):
self.data = data
self.type = "JSON"
class XMLParser(DataParser):
def __init__(self, data):
self.data = data
self.type = "XML"
class PlainTextParser(DataParser):
def __init__(self, data):
self.data = data
self.type = "Plain Text"
parser1 = DataParser({"key": "value"})
parser2 = DataParser("<?xml version='1.0'?>")
parser3 = DataParser("Hello, world!")
print(type(parser1).__name__) # JSONParser
print(type(parser2).__name__) # XMLParser
print(type(parser3).__name__) # PlainTextParser
Object Caching and Memoization
class CachedUser:
_cache = {}
def __new__(cls, user_id):
if user_id in cls._cache:
print(f"Returning cached user {user_id}")
return cls._cache[user_id]
print(f"Creating new user {user_id}")
instance = super().__new__(cls)
cls._cache[user_id] = instance
return instance
def __init__(self, user_id):
# Only initialize if this is a new instance
if not hasattr(self, 'user_id'):
self.user_id = user_id
self.data = f"Data for user {user_id}"
user1 = CachedUser(1)
user2 = CachedUser(2)
user3 = CachedUser(1) # Returns cached instance
print(user1 is user3) # True
Common Pitfalls and Best Practices
Pitfall 1: Returning a Value from init
class WrongInit:
def __init__(self, value):
self.value = value
return self # TypeError: __init__() should return None
# Correct version:
class CorrectInit:
def __init__(self, value):
self.value = value
# Implicit return None
Pitfall 2: Forgetting to Return from new
class WrongNew:
def __new__(cls):
instance = super().__new__(cls)
# Forgot to return!
obj = WrongNew()
print(obj) # None
# Correct version:
class CorrectNew:
def __new__(cls):
instance = super().__new__(cls)
return instance
Pitfall 3: Not Calling super().new()
class ForgotSuper:
def __new__(cls):
return object() # Returns plain object, not ForgotSuper instance
obj = ForgotSuper()
print(type(obj)) # <class 'object'>, not ForgotSuper!
# Correct version:
class RememberSuper:
def __new__(cls):
return super().__new__(cls)
Best Practice: When NOT to Override
Don’t override __new__ unless you have a specific reason. The default implementation works perfectly for 99% of cases. Override only when you need to:
- Subclass immutable types
- Implement singletons or object pools
- Control which class gets instantiated (factory pattern)
- Intercept instance creation at a fundamental level
For normal attribute initialization, always use __init__.
Conclusion
Python’s two-phase object creation gives you fine-grained control over instantiation. __new__ handles memory allocation and instance creation, while __init__ handles initialization. For everyday programming, stick with __init__. But when you need to subclass immutable types, implement design patterns like singletons or factories, or control instance creation at a deeper level, __new__ is your tool.
Remember: __new__ creates, __init__ configures. Master this distinction, and you’ll unlock advanced Python patterns that most developers never explore.