Python Dunder methods - __new__, __call__ and __init__

This write up deals with exploring the object creation aspect in Python. Specifically, it attempts to answer following queries:
How are Python objects created?
How can we change that for special use cases?
What magic takes place in this process? We are going to answer all these questions.
We'll take a deep dive in to dunder (double underscore) methods in Python, specially methods used in creation and initialization of objects.
Here is a normal class definition looks like:
class MyClass:
# attributes
# and
# method definitions here
def __init__(self, *args, **kwarg):
# Initialize *object* attributes
Lot of people thing that __init__
is a constructor but it is not the case. If you look closely, the first argument is self
which is a convention for the object instance and not the class
. You can name it whatever you want, but the first argument passed to __init__
is always the object instance. This means __init__
is not the constructor and the object is created before control reaches it.
So who is responsible to create the object instance? This is where __new__
comes in and is quite logical as new is named default constructor in most object oriented programming languages. In most cases, we do not need to define __new__
as the synopsis below suggests, it just creates an instance of class:
def __new__(cls, *args, **kwargs):
return super(Class_1, cls).__new__(cls, *args, **kwargs)
Note that it gets the class
as its first argument and it calls the base constructor and returns an instance. This also implies that __new__
is a static
method (or class method), it operates on class and not on instance.
But, does something else happens before constructor is called? We instantiate an object os a class via:
instance = MyCLass()
If you notice, MyCLass()
looks like a function call, yes? This is where another dunder method __call__
comes in. In short __call__
is invoked every time when the class is invoked as a function. And __call__
is also a static method and it also gets class as its first argument. Here is where you can control how an instance needs to be created or even if it needs to be created (singleton?).
Here is a snippet to understand the order in all of this object instantiation process:
Read more about metaclasses here
class Meta(type):
def __call__(cls, *args, **kwargs):
print(f"entering Meta.__call__(): {cls}")
rv = super(Meta, cls).__call__(*args, **kwargs)
print("exiting Meta.__call__()")
return rv
class MyClass(metaclass=Meta):
def __new__(cls, *args, **kwargs):
print( "entering MyClass.__new__()")
rv = super(MyClass, cls).__new__(cls, *args, **kwargs)
print ("exiting MyClass.__new__()")
return rv
def __init__(self, *args, **kwargs):
print ("executing MyClass.__init__()")
super(MyClass,self).__init__(*args, **kwargs)
instance = MyClass() # Instantiate
The output shows everything is wrapped in __call__
method:
entering Meta.__call__(): <class '__main__.MyClass'>
entering MyClass.__new__()
exiting MyClass.__new__()
executing MyClass.__init__()
exiting Meta.__call__()
So __call__
is the best place to control how an object of that class is created. For example we can define a Singleton class which can be used as a metaclass
of any class for which we want to create exactly one (single) instance.
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
Any class that declares Singleton
as its metaclass will ever create only one instance.
class Child(metaclass=Singleton):
name = None
def __init__(self, name):
self.name = name
child1 = Child('child1')
child2 = Child('child2')
assert child1 is child2
child1
is child2
.
Conclusion
We can control object creation using __call__
method and do many other things by defining it in a metaclass
, for example, creating a class based decorators. That, is for another time :).
Subscribe to my newsletter
Read articles from Rajesh Pethe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Rajesh Pethe
Rajesh Pethe
Passionate software engineer with 17+ years of experience in design and development of full life cycle commercial applications. Functional experience include Financial, Telecom and E-Commerce applications. Primary technical stack includes but not limited to Python, Django, REST, SQL, Perl, Unix/Linux. Secondary technical skills include Java, Angular and React JS. DevOps skills include CiCD, AWS, Docker, Kubernetes and Terraform.