__init__
__init__(self, /, *args, **kwargs) Purpose and Behavior
The __init__ method is a special initialization hook that Python calls automatically after an object is created by __new__. It is not a constructor in the traditional sense—__new__ handles object allocation and creation, while __init__ initializes the object’s state by setting up instance attributes.
The lifecycle of object creation follows this sequence:
__new__allocates memory and returns an instance- Python binds the returned instance to
self - Python calls
__init__(self, ...)with any remaining arguments - The fully initialized object is returned to the caller
If __new__ returns an instance of a different class, the __init__ of the returned instance’s class is called (not the original class’s __init__).
Syntax and Parameters
def __init__(self, /, *args, **kwargs):
# self is automatically bound to the instance
...
| Parameter | Description |
|---|---|
self | Reference to the newly created instance (automatically bound) |
*args | Positional arguments passed to the class call |
**kwargs | Keyword arguments passed to the class call |
Default arguments work as expected:
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
p = Point(1, 2) # x=1, y=2
p2 = Point() # x=0, y=0
Object Lifecycle
Understanding when each dunder method runs is essential for debugging:
__new__runs first—before the instance exists. It creates and returns the instance.__init__runs second—the instance already exists. It configures the instance.- Both are automatically called when you instantiate:
MyClass(arg1, arg2)
If __init__ raises an exception, object creation fails and the partially initialized object may be garbage collected.
Common Use Cases
Setting Instance Attributes
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
Validation and Transformation
class Counter:
def __init__(self, start=0):
if start < 0:
raise ValueError("start must be non-negative")
self.value = start
Computed Defaults
class Log:
def __init__(self, filename):
self.filename = filename
self.entries = []
self.created_at = datetime.now()
Important Constraints
Cannot Return Non-None
class Broken:
def __init__(self):
return "oops" # TypeError: __init__() should return None, not 'str'
Don’t Call init Directly on Instances
class MyClass:
def __init__(self, value):
self.value = value
obj = MyClass(10)
obj.__init__(20) # Re-initializes but creates confusing state
MyClass.__init__(obj, 30) # Works but usually unnecessary
Use super().init() for Inheritance
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call parent's __init__
self.breed = breed
Forgetting to call super().__init__() in subclasses leaves parent attributes unset.
Factory Patterns
When __init__ alone cannot express your initialization logic, use class methods as factories:
class Fraction:
def __init__(self, numerator, denominator):
self.numerator = numerator
self.denominator = denominator
@classmethod
def from_string(cls, s):
n, d = s.split('/')
return cls(int(n), int(d))
@classmethod
def from_decimal(cls, value, precision=1000):
return cls(int(value * precision), precision)
# Usage
f1 = Fraction(1, 2)
f2 = Fraction.from_string("3/4")
f3 = Fraction.from_decimal(0.5)
This pattern lets callers use descriptive names while still invoking __init__ internally.
Dataclasses
For simple data-holding classes, Python 3.7+ dataclasses reduce boilerplate:
from dataclasses import dataclass
@dataclass
class Point:
x: float = 0.0
y: float = 0.0
# The dataclass decorator automatically generates:
# - __init__(self, x=0.0, y=0.0)
# - __repr__
# - __eq__
p = Point(1.0, 2.0)
Dataclasses also support __post_init__ for validation after auto-generated initialization:
from dataclasses import dataclass, field
@dataclass
class Inventory:
items: dict = field(default_factory=dict)
def __post_init__(self):
if not isinstance(self.items, dict):
raise TypeError("items must be a dict")
inv = Inventory({"apples": 5}) # Works
# Inventory(items={}) is automatically created when omitted
Use regular __init__ when you need full control over initialization logic, or dataclasses when you want minimal boilerplate for data objects.
Type Hints
Modern Python code often includes type hints in __init__ for better IDE support and documentation:
from typing import Optional
class User:
def __init__(self, name: str, age: int, email: Optional[str] = None) -> None:
self.name = name
self.age = age
self.email = email
Note that __init__ still returns None implicitly—explicitly writing -> None is optional but improves readability.
Multiple Inheritance
With multiple inheritance, __init__ calls follow the Method Resolution Order (MRO):
class Flyer:
def __init__(self, speed: int):
self.speed = speed
class Swimmer:
def __init__(self, depth: int):
self.depth = depth
class Duck(Flyer, Swimmer):
def __init__(self, speed: int, depth: int, name: str):
super().__init__(speed) # Calls Flyer.__init__
Swimmer.__init__(self, depth) # Explicit call to Swimmer
self.name = name
duck = Duck(50, 10, "Donald")
# speed from Flyer, depth from Swimmer, name from Duck
The super().__init__() call follows MRO (Flyer → Swimmer → object), so only the first base class’s __init__ is called automatically. Call other base __init__ methods explicitly when needed.
See Also
__new__— Object creation, runs before__init__(coming soon)__repr__— String representation for debugging (coming soon)- Abstract base classes — Designing class hierarchies
- Dataclasses — Simplified initialization with decorators
Conclusion
The __init__ method is the standard way to initialize Python objects after creation. Unlike constructors in some languages, it runs after __new__ has already allocated the object, giving you a fully-formed instance to configure.
Key takeaways:
__init__cannot return a value other thanNone—doing so raises aTypeError- Always call
super().__init__()in subclasses to initialize parent attributes - For complex construction logic, consider factory methods via classmethods
- For simple data objects, dataclasses eliminate most
__init__boilerplate
Mastering __init__ is foundational to writing clean, maintainable Python classes.