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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.