Python

Classes in Python

Advanced

Classes in Python

__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

The signature of your __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

← All training