pyguides

__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:

  1. __new__ allocates memory and returns an instance
  2. Python binds the returned instance to self
  3. Python calls __init__(self, ...) with any remaining arguments
  4. 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
    ...
ParameterDescription
selfReference to the newly created instance (automatically bound)
*argsPositional arguments passed to the class call
**kwargsKeyword 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 than None—doing so raises a TypeError
  • 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.