What Really Creates Python Objects? Unveiling __new__ vs __init__
This article explains how Python actually creates objects, clarifying the distinct roles of the __new__ and __init__ methods, and demonstrates practical techniques such as inheriting immutable types, adding metadata, and implementing singleton patterns through custom object creation.
Ever wondered whether
__init__is a constructor? In Python, object creation is split into two steps:
__new__allocates memory and returns a new instance, while
__init__initializes that instance’s state.
1. Theory: Object creation in Python
Python creates objects internally before calling the initializer. Understanding this mechanism lets you manipulate object creation for advanced use‑cases.
How to create objects in Python?
You simply instantiate a class, e.g.
SimpleObject(name="bob"), or create built‑in types like
stror
int. The following class defines an
__init__method and a
say_hellomethod:
<code>class SimpleObject:
greet_name: str
def __init__(self, name: str):
self.greet_name = name
def say_hello(self) -> None:
print(f"Hello {self.greet_name}!")
my_instance = SimpleObject(name="bob")
my_instance.say_hello()
</code>Note that
__init__receives the
nameargument and stores it in the
greet_nameattribute, preserving the instance’s state.
__init__ is a constructor?
Technically, no. A constructor creates the object;
__init__only sets its state after the object has already been created.
What does __new__ do?
__new__is a static method called on the class itself. It must return an instance of the class and is responsible for the actual memory allocation.
Where does __new__ come from?
Everything in Python is an object, and the base
objectclass defines
__new__. Even a class that does not explicitly inherit anything is still an
objectsubclass.
<code>my_instance = SimpleObject(name="bob")
print(isinstance(my_instance, object)) # True
print(isinstance(42, object)) # True
print(isinstance('hello', object)) # True
print(isinstance({"my": "dict"}, object)) # True
</code>Difference between __new__ and __init__
__new__creates the object (allocates memory and returns it). Once the object exists,
__init__runs to initialize its attributes.
Python's object creation process
When a new object is created, Python executes the following functions in order:
__new__: allocate memory and return a new object
__init__: initialize the newly created object
Overriding
__new__lets you intervene in this process:
<code>class SimpleObject:
greet_name: str
def __new__(cls, *args, **kwargs):
print("__new__ method")
return super().__new__(cls)
def __init__(self, name: str):
print("__init__ method")
self.greet_name = name
def say_hello(self) -> None:
print(f"Hello {self.greet_name}!")
my_instance = SimpleObject(name="bob")
my_instance.say_hello()
</code>Running this code prints:
<code>__new__ method
__init__ method
Hello bob!
</code>The output confirms that
__new__runs before
__init__. The default
__new__simply returns
super().__new__(cls):
<code>class SimpleObject:
def __new__(cls, *args, **kwargs):
return super().__new__(cls)
</code>Practical Application 1: Inheriting immutable types
By overriding
__new__, you can inherit from immutable built‑ins like
tuple. The following
Pointclass stores coordinates in a tuple while adding custom methods and validation:
<code>class Point(tuple):
x: float
y: float
def __new__(cls, x: float, y: float):
if x < 0 or y < 0:
raise ValueError("x and y must be positive")
return super().__new__(cls, (x, y))
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def distance_from(self, other_point: 'Point'):
import math
return math.sqrt((other_point.x - self.x) ** 2 + (other_point.y - self.y) ** 2)
p = Point(1, 2)
p2 = Point(3, 1)
print(p.distance_from(p2)) # 2.23606797749979
</code>Practical Application 2: Adding metadata
You can derive from
floatand attach extra information, such as a currency symbol:
<code>class Currency(float):
def __new__(cls, value: float, symbol: str):
obj = super(Currency, cls).__new__(cls, value)
obj.symbol = symbol
return obj
def __str__(self) -> str:
return f"{self.symbol}{self:.2f}"
price = Currency(12.768544, symbol='€')
print(price) # €12.77
print(isinstance(price, float)) # True
print(f"{price.symbol}{price * 2}") # €25.54
</code>Practical Application 3: Singleton pattern
Using
__new__you can ensure a class has only one instance:
<code>class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2) # True
</code>Other practical uses
Control instance creation : Add validation, logging, or modification before an object is created.
Factory methods : Decide which subclass to instantiate inside
__new__.
Caching : Return a previously created object instead of allocating a new one.
Conclusion
This article explored Python’s object creation pipeline, clarified the distinct responsibilities of
__new__and
__init__, and demonstrated how overriding
__new__enables advanced patterns such as immutable inheritance, metadata enrichment, and singletons, leading to more efficient and expressive code.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.