__init__ vs __new__: What Actually Happens When You Create an Object
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):
MyClass.__new__(MyClass, arg1, arg2) to create a new instance__new__ returns an instance of MyClass, Python then calls instance.__init__(arg1, arg2) to initialize itobjNotice 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.
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:
int, str, tuple, and frozenset can't be changed after creation, so __init__ is too lateWith 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__().
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:
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:
class Bad:
def __new__(cls, x):
self = super().__new__(cls)
self.x = x # Works, but wrong place
return self
# No __init__ — attributes set too earlyclass Good:
def __new__(cls, x):
return super().__new__(cls)
def __init__(self, x):
self.x = x # Correct placeWhile 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
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)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).
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.
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
20Create 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.