Back to Blog
Python

Python __init__: How Object Initialization Works

python **init**: Learn how Python's __init__ method initializes objects, handles inheritance, and avoids common pitfalls in class design.

PythonObject-Oriented ProgrammingClass InitializationConstructorInheritance
Illustration of a Python class blueprint transforming into a concrete object with attribute labels, representing the __init__ constructor method.

When you define a class in Python, __init__ is the method that controls how each new instance is set up. It is the first place where you assign attributes to self and enforce invariants that the object needs to work correctly. Understanding python **init** is essential for writing classes that are predictable and easy to maintain.

The Role of init in Python Classes

__init__ is not a constructor in the strict sense. In Python, the actual object allocation happens in __new__, which returns a new instance. __init__ is called immediately after that, receiving the new instance as self along with any arguments passed to the class call. Its job is to to initialize the instance's state, not to create it. This distinction matters when you need to control object creation itself, but for most classes, __init__ is the only method you override.

When you write obj = MyClass(arg), Python performs two steps: first it calls MyClass.__new__(MyClass, arg) to get an instance, then it calls obj.__init__(arg) to initialize that instance. If __init__ raises an exception, the object is not returned to the caller, and the allocation is discarded. This makes __init__ the right place to validate input and set up attributes that the object's methods will rely on.

Basic Syntax and Usage

A minimal class with an __init__ method looks like this:

class Point: def __init__(self, x, y): self.x = x self.y = y

When you call Point(3, 4), Python creates a new instance, passes it as self, and assigns x and y to that instance. The method can accept any number of parameters, including default values, keyword-only arguments, and *args/**kwargs for flexible signatures. The self parameter is always the instance itself, and you should not pass it explicitly when calling the class.

You can also define __init__ without parameters if the object needs no initial state:

class Logger: def __init__(self): self.entries = []

This is perfectly valid, but in practice you will often pass data in to avoid mutating shared state later.

__init__ vs __new__: Initializer vs Allocator

__new__ is a static method that receives the class as its first argument and returns a new instance. It is responsible for allocation, and it runs before __init__. For most classes, you never override __new__; the default implementation creates an instance and passes it to __init__. Overriding __new__ is necessary only when you need to control allocation itself, such as when implementing singletons, immutable types, or metaclasses.

A common misconception is that __init__ is the constructor. In Python, the constructor is the __call__ method of the metaclass, which invokes __new__ and then __init__. For practical purposes, you can treat __init__ as the place to set up instance state, and __new__ as the place to control instance creation. If you override both, remember that __new__ must return an instance, and __init__ will only be called if the returned object is an instance of the class. If __new__ returns an object of a different type, __init__ is skipped.

Handling Inheritance with super().__init__()

When a subclass defines its own __init__, it must explicitly call the parent's __init__ if the parent needs to set up its own attributes. This is done with super().__init__(...). Failing to do so leaves the parent's state uninitialized, which often leads to AttributeError or subtle bugs later.

class Vehicle: def __init__(self, make, model): self.make = make self.model = model class Car(Vehicle): def __init__(self, make, model, doors): super().__init__(make, model) self.doors = doors

Here, super().__init__(make, model) sets make and model on the instance, and then Car adds doors. The order matters: you should call super().__init__ before accessing any attributes that the parent initializes, unless you have a specific reason to delay it. In multiple inheritance scenarios, super() follows the MRO (method resolution order), so calling it in each class ensures cooperative initialization as long as every class in the chain uses the same pattern.

Common Mistakes and How to Avoid Them

One frequent error is using a mutable default argument in __init__. Defaults are evaluated once at function definition time, so all instances share the same list or dictionary if you write:

class Bad: def __init__(self, items=[]): self.items = items

Instead, use None and create a new collection inside the method:

class Good: def __init__(self, items=None): self.items = items if items is not None else []

Another mistake is trying to return a value from __init__. The method must return None; returning anything else raises TypeError at runtime. This is a design constraint: initialization is not supposed to produce a value, it is supposed to mutate the instance.

A third common issue is forgetting to call super().__init__ in a subclass. This is especially easy when the parent class has no explicit __init__, because the default object constructor takes no arguments. If the parent defines its own __init__, the subclass must call it, or the parent's attributes will be missing.

Performance and Memory Considerations

__init__ runs on every instance creation, so heavy work inside it directly affects object construction time. If you are creating many objects in a loop, avoid expensive operations such as file I/O, network calls, or complex computations in __init__. Defer such work to a method that runs only when needed.

Memory usage can also be reduced with __slots__. By default, each instance has a __dict__ that stores its attributes, which is flexible but memory-heavy. Declaring __slots__ in a class fixes the attribute names and eliminates the per-instance dictionary, which can significantly reduce memory for classes with millions of instances. However, __slots__ prevents adding new attributes dynamically, so use it only when the attribute set is known in advance.

class SlotPoint: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y

This class behaves like the earlier Point, but instances use less memory. The tradeoff is that you lose the ability to attach arbitrary attributes, which is rarely needed in well-designed code.

Advanced Patterns: Classmethod Constructors and Validation

Sometimes you want to create an instance from data that is not in the same format as the __init__ parameters. A common pattern is to define a classmethod that preprocesses the input and then calls the normal constructor:

class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls,, iso_date): year, month, day = map(int, iso_date.split('-')) return cls(year, month, day)

This gives you a clear alternative constructor without complicating the main __init__ signature. You can also put validation logic directly in __init__ to ensure the object is always in a valid state. For example, a Temperature class could reject values below absolute zero:

class Temperature: def __init__(self, celsius): if celsius < -273.15: raise ValueError("Temperature cannot be below absolute zero") self.celsius = celsius

Raising early in __init__ prevents the object from ever existing in an invalid state. This is a straightforward way to enforce invariants, though for more complex validation you might prefer a separate method or a descriptor. The key is that __init__ is the first opportunity to establish the object's contract, and using it well makes the rest of the class simpler to reason about.

python **init**: Practical Usage and Code Examples | RYUSLOG DEV