Understanding the Python __init__ Method
python **init** method: Learn how Python's __init__ method initializes objects, why it differs from __new__, and how to handle inheritance, mutable defaults, and runti...
The python **init** method is the initializer that runs when you create a new instance of a class. In Python, __init__ is not the constructor; the constructor is __new__, which creates the instance. __init__ receives the already-created instance and sets up its initial state. This distinction matters because it affects how you control object creation, how you handle inheritance, and where you place validation logic.
What __init__ Actually Does
When you write obj = MyClass(arg), Python calls type(obj).__call__, which internally invokes __new__ to allocate the instance and then __init__ to initialize it. If __new__ returns an instance of the class, __init__ is called automatically with the same arguments. If __new__ returns something else, __init__ is skipped.
class User: def __init__(self, name, email): self.name = name self.email = email
The first parameter self is the instance being initialized. You do not pass it explicitly; Python supplies it automatically. The remaining parameters are whatever you pass to the class call. The method must return None; returning any other value raises TypeError at runtime.
The Difference Between __new__ and __init__
__new__ is a static method that creates the instance. __init__ is an instance method that initializes it. Most classes only need __init__. You override __new__ only when you need to control instance creation itself, such as implementing singletons or returning cached instances.
class Singleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self): # This runs every time Singleton() is called self.created = True
Note that __init__ runs even when __new__ returns an existing instance. If you want initialization to happen only once, you need to guard it explicitly with a flag.
| Aspect | __new__ | __init__ |
|---|---|---|
| Role | Creates the instance | Initializes the instance |
| Called when | Before __init__ | After __new__ |
| Return value | Instance | None |
| Typical use | Control allocation | Set up instance state |
Using super().__init__() in Inheritance
When a subclass defines its own __init__, it does not automatically call the parent's __init__. If the parent sets up required state, you must call super().__init__().
class Base: def __init__(self, value): self.value = value class Child(Base): def __init__(self, value, extra): super().__init__(value) self.extra = extra
Calling super().__init__() first ensures the parent's invariants are established before the subclass adds its own attributes. The order matters when the parent's initialization depends on attributes that the subclass might override. In multiple inheritance, super() follows the MRO, so the call chain can be more complex than a single parent call.
Mutable Default Arguments
A common mistake is using a mutable default in __init__:
class Cart: def __init__(self, items=[]): # Wrong self.items = items
The default list is created once at function definition time and shared across all instances. Any mutation to self.items on one instance affects every other instance that used the default. Use None instead:
class Cart: def __init__(self, items=None): self.items = items if items is not None else []
This is a classic Python pitfall that affects any function with mutable defaults, but it shows up often in __init__ because object state is the whole point.
Runtime Cost and Object Creation
Every call to a class runs __init__. If __init__ performs heavy work, object creation becomes expensive. For frequently created objects, keep initialization minimal and defer expensive operations to lazy properties or explicit methods.
class Report: def __init__(self, data): self.data = data self._summary = None @property def summary(self): if self._summary is None: self._summary = self._compute_summary() return self._summary
This avoids recomputing or eagerly computing work that may never be needed. The tradeoff is that the object holds a reference to the raw data longer, which can matter for memory usage in long-lived objects.
Common Failure Modes
If __init__ raises an exception, the object is not returned to the caller, but __new__ has already allocated it. The partially constructed instance is garbage collected. This means you cannot rely on the instance existing if initialization fails.
If you forget to call super().__init__() in a subclass, the parent's attributes are missing, which produces AttributeError at first access. The error message often points to the attribute access, not the missing call, which makes the root cause harder to find.
Another failure mode is assigning to an attribute that does not exist on the class when __slots__ is used:
class Fixed: __slots__ = ("x",) def __init__(self, x, y): self.x = x self.y = y # AttributeError: 'Fixed' object has no attribute 'y'
__slots__ restricts which attributes an instance can hold. If __init__ assigns an attribute not listed in __slots__, Python raises AttributeError immediately.
When __init__ Should Not Be Used
For immutable data or simple data containers, consider dataclasses. Dataclasses generate __init__ for you:
from dataclasses import dataclass @dataclass class Point: x: float y: float
The generated __init__ includes type annotations and default handling. If you need custom validation, you can still write __post_init__ in a dataclass, which runs after the generated __init__. This keeps the boilerplate down while preserving the ability to enforce invariants at creation time. For classes that only carry data, a dataclass is usually clearer than a hand-written __init__.