Skip to main content
Editor

__init__ vs __new__: What Actually Happens When You Create an Object

Advanced25 min5 exercises100 XP
0/5 exercises

When you write obj = MyClass(), it feels like one simple step. But under the hood, Python actually does two things: it creates the object, and then it initializes it. These are handled by two different methods - __new__ and __init__.

Think of it like building a house. __new__ is the construction crew that pours the foundation and puts up the walls - it creates the empty structure. __init__ is the interior designer who comes in afterward and sets up the furniture, paints the walls, and makes it livable.

Most of the time, you only need __init__. But understanding __new__ unlocks powerful patterns like singletons, caching, and customizing immutable types.

What Happens Step by Step When You Call MyClass()?

Here's the exact sequence Python follows when you write obj = MyClass(arg1, arg2):

  • Python calls MyClass.__new__(MyClass, arg1, arg2) to create a new instance
  • If __new__ returns an instance of MyClass, Python then calls instance.__init__(arg1, arg2) to initialize it
  • The initialized instance is assigned to obj
  • Watching the creation flow
    Loading editor...

    Notice the order: __new__ runs first and returns a raw, empty object. Then __init__ runs on that object and fills in the attributes. By the time you get the object back, both steps are done.

    How Does __new__ Work?

    __new__ is a static method (technically, it's special-cased by Python). It receives the class as its first argument (cls, not self) and must return an instance. The critical thing: __new__ must call super().__new__(cls) to actually allocate the object in memory.

    __new__ receives cls, not self
    Loading editor...

    A key rule: both __new__ and __init__ receive the same arguments (besides cls/self). Python passes whatever you put in MyClass(...) to both methods.

    When Do You Actually Need __new__?

    Most classes only need __init__. You need __new__ in three specific situations:

  • Subclassing immutable types - int, str, tuple, and frozenset can't be changed after creation, so __init__ is too late
  • Singleton pattern - returning the same instance every time
  • Instance caching - reusing previously created instances
  • Subclassing an immutable type
    Loading editor...

    With str, the string value is baked in at creation time. By the time __init__ runs, the string content is already frozen. That's why we must use __new__ to pass the uppercased value to super().__new__().

    Singleton pattern with __new__
    Loading editor...

    Can You Cache and Reuse Instances?

    A more flexible pattern than singleton is instance caching. Instead of forcing one instance, you reuse instances based on their arguments:

    Connection pooling with __new__
    Loading editor...

    The _initialized flag solves the problem of __init__ running repeatedly. On the first creation, attributes are set. On subsequent calls with the same arguments, __init__ sees the flag and skips re-initialization.

    What Are Common Mistakes with __new__?

    There are a few traps developers fall into when working with __new__. Let's look at the most common ones:

    Wrong: Setting attributes in __new__
    class Bad:
        def __new__(cls, x):
            self = super().__new__(cls)
            self.x = x  # Works, but wrong place
            return self
        # No __init__ - attributes set too early
    Right: Use __init__ for attributes
    class Good:
        def __new__(cls, x):
            return super().__new__(cls)
        
        def __init__(self, x):
            self.x = x  # Correct place

    While setting attributes in __new__ technically works, it breaks convention and can cause confusion. The rule of thumb: __new__ handles creation, __init__ handles initialization.


    Practice Exercises

    Predict the Creation Order
    Predict Output

    What does the following code print?

    class Widget:
        def __new__(cls, name):
            print(f'new: {name}')
            instance = super().__new__(cls)
            return instance
        
        def __init__(self, name):
            print(f'init: {name}')
            self.name = name
    
    w = Widget('button')
    print(w.name)
    # Type your predicted output
    
    Subclass an Immutable Type
    Write Code

    Create a class ClampedInt that subclasses int. It should clamp values to be between 0 and 255 (inclusive). Values below 0 become 0, values above 255 become 255.

    Since int is immutable, you must use __new__ to set the value.

    Print the results for ClampedInt(-50), ClampedInt(100), and ClampedInt(999).

    class ClampedInt(int):
        # Your code here
        pass
    
    print(ClampedInt(-50))
    print(ClampedInt(100))
    print(ClampedInt(999))
    
    Build a Singleton
    Write Code

    Create a Logger class that implements the singleton pattern using __new__. No matter how many times you call Logger(), it should always return the same instance.

    Add an __init__ that sets self.logs to an empty list only on the *first* initialization (use a guard flag).

    Add a log(self, message) method that appends the message to self.logs.

    Create two Logger instances, log a message on each, then print the length of logs from the first instance.

    class Logger:
        # Your singleton code here
        pass
    
    a = Logger()
    b = Logger()
    a.log('error 1')
    b.log('error 2')
    print(a is b)
    print(len(a.logs))
    
    Fix the Immutable Subclass
    Fix the Bug

    This code tries to create a NamedTuple-like class by subclassing tuple, but the values aren't being set correctly. Fix it.

    Expected output:

    (10, 20)
    10
    20
    class Point(tuple):
        def __init__(self, x, y):
            # Bug: trying to set values in __init__
            # but tuple is immutable!
            super().__init__((x, y))
        
        @property
        def x(self):
            return self[0]
        
        @property
        def y(self):
            return self[1]
    
    p = Point(10, 20)
    print(p)
    print(p.x)
    print(p.y)
    
    Build an Instance Cache
    Write Code

    Create a class Symbol that caches instances by name. If you call Symbol('x') twice, you should get the exact same object back. Use __new__ with a class-level dictionary for caching.

    Add a guard in __init__ to prevent re-initialization.

    Test it: create a = Symbol('x'), b = Symbol('x'), and c = Symbol('y'). Print whether a is b and whether a is c.

    class Symbol:
        # Your code here
        pass
    
    a = Symbol('x')
    b = Symbol('x')
    c = Symbol('y')
    print(a is b)
    print(a is c)
    

    Related Tutorials

    Was this tutorial helpful?