Python
Classes in Python
Advanced
__new__
and
__init__
play distinct roles in Python object creation. While
__new__
is responsible for creating and returning a new instance,
__init__
initializes that already-created object. Understanding their relationship is essential, especially when working with immutability, inheritance, or advanced patterns like singletons, where controlling object creation becomes crucial.
classes in python
A class in Python is a blueprint for creating objects. It consists of class variables, object variables, class methods, object methods, and static methods, similar to some other programming languages. However, there are other kinds of methods called dunder methods, or magic methods, that are built into Python. Two of the most important dunder methods are
__init__
and
__new__
. Before exploring metaclasses in Python, we need to know about them.
instantiating a class
As mentioned above, a class will most likely have some object variables. These variables are tailored to each object that is instantiated from the class. This is where
__init__
comes into play.
__init__
(as its name suggests) initialises an already created instance and assigns the given values to the object variables (and can modify class variables).
Here is a simple example:
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
return f"{self.name} says: Woof!"
def __repr__(self):
return f"Dog(name='{self.name}', breed='{self.breed}')"
# Instantiate the class
my_dog = Dog(name="Buddy", breed="Golden Retriever")
print(my_dog)
print(my_dog.bark())
You may have noticed that I said
__init__
works with an already created instance. Therefore, instantiating an object is not the duty of
__init__
. This may raise the question of which function instantiates an object from a class.
__new__
creates the instance. Using
__new__
is not common unless we need to control object creation itself, and it runs before
__init__
. If we do not define
__new__
inside a class, the Python interpreter runs the parent's
__new__
, creates an object, and then calls our defined
__init__
. If we define
__new__
, it is called first to create an instance, and then
__init__
runs. If
__new__
does not return an instance of the class,
__init__
is skipped, and no object is created.
Let's have a look at a few simple examples.
Example 1: Immutable object (
int
-like behavior)
class PositiveInt(int):
def __new__(cls, value):
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls, value)
x = PositiveInt(5)
print(x, type(x))
__new__
is required because
int
is immutable, and we need to apply control during object creation to ensure that the assigned value is positive. Otherwise, the object is not created, and a
ValueError
is raised.
Example 2: Singleton pattern
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
print(a is b) # True
Object creation is controlled before initialization. If there is already an instance of this class, the existing object is returned, and the creation of another object is prevented.
Example 3:
__init__
is skipped if
__new__
returns something else
class Weird:
def __new__(cls):
return "not an instance"
def __init__(self):
print("Never called")
w = Weird()
print(w) # "not an instance"
__new__
is responsible for allocating and returning the object. It receives
cls
plus any arguments and returns a new instance.
__new__
is implemented explicitly when we must control instance creation itself, typically for immutable types, singletons, or framework-level abstractions.
__init__
receives that already-created object as
self
and arguments to set it up. By the time
__init__
runs, the object already exists.
Looking at the above examples shows that calling the parent
__new__
is mandatory. Otherwise, even if the condition for creating the class is met, the object is never created. Why is
super().__new__(cls)
always required? Without calling
super().__new__(cls)
, no object gets allocated—your method returns
None
and Python crashes. It is the one non-negotiable step: something in the chain must call
object.__new__
to actually reserve memory for the instance.
The MRO chain always ends at object. Every Python class implicitly inherits from object. When your class doesn't inherit from anything explicit, super().__new__ is object.__new__. When it inherits from int, the chain is PositiveInt → int → object, so super().__new__ hits int.__new__ first.
This is the point where we should be careful with the signature of
__new__
, otherwise we might run into errors. This is directly related to class inheritance. If the defined class is a plain class, meaning it inherits from
object
(the highest parent of all in Python), then it is straightforward. We can say we might have four scenarios:
Plain class (inherits
object
)
When your class inherits from
object
, which is every plain class,
object.__new__
only needs
cls
. It just allocates a blank shell in memory. It has no concept of a "value". That's
__init__
's job.
The four scenarios
1) No __init__, passes value to super, in this scenario, object (main parent) rejects value and raises TypeError
class PositiveInt:
def __new__(cls, value): # accept value — Python sends it here
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls, value) # TypeError — object rejects value
2) No __init__, passes only cls, this scenario works fine, but, since there is no __init__, the instance of class is created, but there is no such attribute
class PositiveInt:
def __new__(cls, value): # accept value — Python sends it here
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls) # creates and returns the instance
x = PositiveInt(5)
print(x) # <__main__.PositiveInt object at 0x000001ACE0B20F10>,
print(x.value) # AttributeError: 'PositiveInt' object has no attribute 'value'
3) Has __init__, passes only cls, this works and is the ideal pattern.
class PositiveInt:
def __new__(cls, value): # accept value — Python sends it here
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls) # do NOT forward value to object.__new__
def __init__(self, value):
self.value = value * 2
x = PositiveInt(5)
print(x) # <__main__.PositiveInt object at 0x000001ACE0B20F10>, created object with super.__new__(cls)
print(x.value) # 10 , set by __init__
The key insight: Python sends the argument 5 to
both
__new__
and
__init__
. Your
__new__
signature must accept it — but you don't forward it to
super()
.
4) Has __init__, passes value to super, this one raises TypeError like scenario one, because object (parent class) never takes value
class PositiveInt:
def __new__(cls, value): # accept value — Python sends it here
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls, value) # # TypeError — object rejects value
def __init__(self, value):
self.value = value * 2
x = PositiveInt(5)
print(x) # TypeError: object.__new__() takes exactly one argument (the type to instantiate)
Inheriting from an immutable (
int
,
str
,
tuple
)
Immutable types bake their value into memory at allocation time. There is no second chance — by the time
__init__
runs, the object's core value is already sealed. This is why
int.__new__
must
receive the value:
#Correct pattern, subclassing int
class PositiveInt(int):
def __new__(cls, value):
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls, value) # int.__new__ needs the value
def __init__(self, value):
self.extra = value * 2 # sits alongside the int, doesn't change it
x = PositiveInt(5)
print(x) # 5 — the int value, locked in __new__
print(x.extra) # 10 — a separate attribute
What happens internally:
PositiveInt(5)
└─► PositiveInt.__new__ validates, calls int.__new__(cls, 5)
└─► int bakes 5 into memory, calls object.__new__(cls) internally
└─► PositiveInt.__init__ adds self.extra = 10 # separate attribute
Inheriting from a mutable (
list
,
dict
)
Mutable types are more relaxed, their content can be changed after creation, so the value doesn't need to be baked in at
__new__
time. Both of these work:
# Either approach is valid for mutables
class MyList(list):
def __new__(cls, value):
return super().__new__(cls) # fine — list can be populated later
def __init__(self, value):
super().__init__(value) # list.__init__ populates it here
# Or
class MyList1(list):
def __new__(cls, value):
return super().__new__(cls, value) # fine — list can be populated here
list_value= [1, 2, 3]
my_list= MyList(list_value) # <__main__.PositiveInt object at 0x000001ACE0B20F10>, created object with super.__new__(cls)
print(my_list) # [1, 2, 3], list is populated in __init__
my_list1= MyList1(list_value) # <__main__.PositiveInt object at 0x000001ACE0B20F10>, created object with super.__new__(cls, value)
print(my_list1) # [1, 2, 3], list is populated in __new__
Watch out:
if you pass value to
both
__new__
and
__init__
for a mutable, the list gets populated twice. Pick one place and be consistent.
The rule to remember
__new__
must always accept the extra arguments — Python routes them there. But whether you
forward
them to
super().__new__
depends entirely on what that parent expects.
Classes OOP Objects Python