The previous article introduced __init__() alongside self as the basics of defining a class — this one goes deeper specifically into python init constructor behavior: the precise relationship between __init__ and __new__, default values, the mutable-default trap applied to constructors, and how super().__init__() fits into inheritance.
Introduction: What init Does
__init__() is the method Python automatically calls immediately after a new object is created, and it's where you typically set up that object's starting attributes.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
alex = Person("Alex", 30)
print(alex.name, alex.age) # Alex 30"Constructor" vs. the more precise term
You'll commonly hear __init__ casually called "the constructor" — and for everyday purposes, that shorthand is harmless and universally understood. But it's worth knowing the more precise term: __init__ is technically the initializer. By the time __init__ runs, the object already exists — its job is to set up (initialize) that already-existing object's starting state, not to bring it into existence in the first place. Section 3 covers exactly which method actually handles creation.
Default vs. Parameterized Constructors
A "default" constructor
If a class's __init__ takes no arguments beyond self, every object gets set up identically, with fixed starting values:
class Counter:
def __init__(self):
self.count = 0 # every Counter starts at exactly 0, no way to customize this at creation
c1 = Counter()
c2 = Counter()
print(c1.count, c2.count) # 0 0A "parameterized" constructor
More commonly, you'll want __init__ to accept arguments, letting each object start with different data:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
alex = Person("Alex", 30)
sam = Person("Sam", 25)This is the far more typical pattern in real code — a class whose objects genuinely differ from one another in their starting data.
If you don't define init at all
If a class doesn't define __init__ whatsoever, Python still provides an implicit, empty default behind the scenes, allowing you to create a bare object with no custom setup:
class Empty:
pass
e = Empty()
print(e) # <__main__.Empty object at 0x...> — a valid object, just with no custom attributes setThis is rarely useful on its own, but it's worth knowing that "no __init__ defined" doesn't cause any error — it just means object creation skips any custom initialization step entirely.
init vs. new: Who Actually Creates the Object?
The real two-step process
When you write Person("Alex", 30), two separate steps actually happen behind the scenes, in this order:
__new__allocates and returns a new, empty instance of the class.__init__then receives that already-created instance (asself) and populates it with starting data.
class Person:
def __new__(cls, *args, **kwargs):
print("Step 1: __new__ creates the instance")
instance = super().__new__(cls)
return instance
def __init__(self, name, age):
print("Step 2: __init__ sets up the instance")
self.name = name
self.age = age
alex = Person("Alex", 30)
# Step 1: __new__ creates the instance
# Step 2: __init__ sets up the instanceThis confirms the order directly: __new__ genuinely runs first, and only once it has successfully returned an actual instance does __init__ get called on that instance.
Why self proves the object already exists
This is the clearest evidence that __init__ isn't doing the actual creation: it receives self as its very first argument — a reference to an object that must already exist in order to be passed in at all. You can't hand someone a reference to something that doesn't exist yet. Consistent with this, __init__ never returns anything meaningful — if you try to return a value from it, Python raises an error, since __init__ is only ever expected to modify the object it was given, not to produce or return a new one (covered further in Section 5).
When new actually matters
For the overwhelming majority of classes you'll write, you'll never need to touch __new__ at all — __init__ covers everything you need. __new__ becomes genuinely relevant in a handful of more advanced, specialized situations:
Subclassing immutable built-in types (like
str,int, ortuple) — since immutable objects can't be modified after creation, any customization has to happen at the__new__stage, before the object is finalized.Implementing the singleton pattern — ensuring a class only ever has exactly one instance, which requires intercepting object creation itself, not just its subsequent setup.
Metaclasses — a genuinely advanced topic controlling how classes themselves (not just instances) get created, well beyond the scope of this article.
Outside of these specific cases, you can safely treat __init__ as "the constructor" in everyday practice, and never think about __new__ again.
Working with init in Practice
Default parameter values
Just like regular functions (covered in the earlier function arguments article), __init__ parameters can have default values, making them optional at creation time:
class Dog:
def __init__(self, name, breed="Mixed", age=1):
self.name = name
self.breed = breed
self.age = age
rex = Dog("Rex")
print(rex.breed, rex.age) # Mixed 1
buddy = Dog("Buddy", "Labrador", 3)
print(buddy.breed, buddy.age) # Labrador 3rex only supplied a name, letting breed and age fall back to their defaults; buddy explicitly overrode both.
The mutable-default-argument pitfall, applied to constructors
This is the exact same trap covered in the earlier function arguments and classes articles, worth restating specifically in the constructor context, since it's an extremely common place for it to show up:
class ShoppingCart:
def __init__(self, items=[]): # DANGER — a mutable default argument
self.items = items
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.items.append("apple")
print(cart2.items) # ['apple'] — cart2 is unexpectedly affected!Because the default list [] is created exactly once, at the moment __init__ is defined (not each time it's called), every ShoppingCart created without explicitly passing items ends up sharing that exact same list object. The fix, exactly as covered before, is defaulting to None and creating a fresh list inside the method body:
class ShoppingCart:
def __init__(self, items=None):
self.items = items if items is not None else []
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.items.append("apple")
print(cart2.items) # [] — correctly independent nowsuper().init() in a subclass
When one class inherits from another (covered in full in a later article on inheritance), the subclass's __init__ typically needs to call the parent class's __init__ first, to make sure inherited attributes get properly set up before the subclass adds its own:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # call Animal's __init__ first, to set self.name
self.breed = breed # then add Dog's own attribute
rex = Dog("Rex", "Labrador")
print(rex.name, rex.breed) # Rex Labradorsuper().__init__(name) calls Animal's __init__ method, passing self (implicitly) and name along to it — ensuring self.name gets set exactly the way Animal defines it, before Dog's own __init__ continues on to set self.breed.
Common Mistakes and Best Practices
Forgetting to call super().init()
If a subclass defines its own __init__ but forgets to call super().__init__(), any attributes the parent class was supposed to set up simply never get set — not through an obvious error, but through a silently missing attribute that only surfaces later, whenever something tries to actually use it:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
# forgot super().__init__(name) here!
self.breed = breed
rex = Dog("Rex", "Labrador")
print(rex.breed) # Labrador — fine
print(rex.name)
# AttributeError: 'Dog' object has no attribute 'name'This is a genuinely common mistake once you start working with inheritance, and it's worth making a habit of calling super().__init__(...) as the very first line of any subclass's __init__, unless you have a specific, deliberate reason not to.
Accidentally returning a value from init
__init__ must implicitly return None — Python enforces this directly, raising an error if you try to return any other value from it:
class Person:
def __init__(self, name):
self.name = name
return "done" # not allowed
alex = Person("Alex")
# TypeError: __init__() should return None, not 'str'This reinforces the point made in Section 3: __init__'s job is purely to modify the object it was given, not to produce or hand back a separate value of any kind.
Best-practice guidance: keep init lightweight
As a general rule, keep __init__ focused specifically on setting attributes and doing genuinely lightweight setup work. If a class needs more involved initialization logic — validating complex input, performing an expensive calculation, or offering several different ways to construct an object depending on what data is available — that heavier logic generally belongs in dedicated methods, or in alternative constructors implemented as @classmethods (briefly introduced in the earlier decorators article, and covered further in a later article on class design):
class Pizza:
def __init__(self, toppings):
self.toppings = toppings
@classmethod
def margherita(cls):
# a dedicated, named alternative way to construct a Pizza,
# rather than cramming multiple construction paths into __init__ itself
return cls(["tomato", "mozzarella"])
pizza = Pizza.margherita()
print(pizza.toppings) # ['tomato', 'mozzarella']Keeping __init__ itself simple and predictable — set attributes, maybe validate a value or two, and little else — makes objects easier to create correctly, and easier to reason about when something goes wrong.