From the archive

What Does __init__ Do in Python?

A practical guide to object creation, initialization, instance attributes, inheritance, common mistakes, and generated dataclass initializers.

When you call a Python class, Python creates an instance and then gives the class an opportunity to initialize it. The __init__ method is where a class commonly establishes that instance’s starting attributes and checks the values supplied by the caller.

It is often described casually as a constructor, but that description leaves out an important distinction: __new__ creates the instance, while __init__ initializes an instance that already exists.

Object Creation and Initialization Are Separate Steps

Python’s data model documentation describes two different special methods in the construction process:

  1. __new__(cls, ...) creates and returns a new instance.
  2. __init__(self, ...) receives that instance and initializes it before it is returned to the caller.

Most application classes do not need to define __new__. The inherited implementation creates the instance, and the class defines __init__ only when it needs a particular initial state.

An __init__ method must return None, which normally means it should not contain a return statement at all. Returning any other value raises TypeError.

A Minimal __init__ Example

This class accepts two values and assigns them to the new instance:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age


person = Person("Alice", 30)
print(f"{person.name} is {person.age} years old.")

Expected output:

Alice is 30 years old.

Calling Person("Alice", 30) creates the instance and passes the two arguments to Person.__init__. The assignments create name and age attributes on that particular instance.

Why Methods Use self

For a normal instance method, Python passes the instance as the first argument automatically. Python programmers name that parameter self so readers can immediately recognize instance access.

The name is a strong convention, not a reserved Python keyword. Another valid identifier would run, but it would make the code needlessly unfamiliar. The official classes tutorial explains how the instance is inserted into a method call and why the self convention matters.

Attributes written through self belong to the instance:

class Counter:
    def __init__(self, starting_value=0):
        self.value = starting_value

    def increment(self):
        self.value += 1


counter = Counter(4)
counter.increment()
print(counter.value)

Expected output:

5

Each Counter receives its own value attribute. That is different from assigning mutable data directly on the class, where every instance could unintentionally share the same object.

Defaults and Validation

Default parameters can make an initial value optional. The method can also reject a value that would leave the instance in an invalid state:

class Book:
    def __init__(self, title, pages=0):
        if not title.strip():
            raise ValueError("title must not be empty")
        if pages < 0:
            raise ValueError("pages must not be negative")

        self.title = title
        self.pages = pages


book = Book("Python Notes", pages=120)
print(book.title)
print(book.pages)

Expected output:

Python Notes
120

Validation belongs here when it protects the object’s basic invariants. Expensive network requests, unrelated file operations, or work that may need to be retried usually deserve a separate method or service instead of happening implicitly during construction.

Initializing a Subclass with super()

When a subclass defines its own __init__, it should explicitly initialize the relevant base class. super() follows the class’s method resolution order and avoids hard-coding the base-class name:

class Animal:
    def __init__(self, name):
        self.name = name


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed


dog = Dog("Buddy", "Golden Retriever")
print(dog.name)
print(dog.breed)

Expected output:

Buddy
Golden Retriever

The call to super().__init__(name) establishes the name attribute managed by Animal. The subclass then adds breed. This example intentionally stays with single inheritance; cooperative multiple inheritance requires compatible method signatures and a fuller explanation than this introductory guide needs.

Common __init__ Mistakes

Omitting the instance parameter

Python supplies the instance when it calls an instance method. A method that accepts no parameters therefore fails when the class is instantiated:

class MissingInstanceParameter:
    def __init__():
        pass


MissingInstanceParameter()

Expected result: TypeError, because Python supplied one positional argument to a method declared with none.

Returning a value

This example also fails intentionally:

class InvalidReturn:
    def __init__(self):
        return "ready"


InvalidReturn()

Expected result: TypeError, because __init__ returned a non-None value.

Sharing mutable class state accidentally

Create mutable per-instance state inside __init__:

class TaskList:
    def __init__(self):
        self.tasks = []

    def add(self, task):
        self.tasks.append(task)


first = TaskList()
second = TaskList()
first.add("Review notes")

print(first.tasks)
print(second.tasks)

Expected output:

['Review notes']
[]

The official classes tutorial demonstrates why a mutable class variable would instead be shared across instances.

When a Dataclass Can Generate __init__

For a class whose main job is to hold named data, the standard-library @dataclass decorator can generate an initializer and other useful methods:

from dataclasses import dataclass


@dataclass
class Point:
    x: float
    y: float


point = Point(3.0, 4.0)
print(point)

Expected output:

Point(x=3.0, y=4.0)

A dataclass is not required for every class, but it can remove repetitive initialization code when its generated behavior matches the design.

The Practical Rule

Use __init__ to leave a newly created instance in a clear, valid starting state. Remember that Python has already created the object before __init__ runs, keep the conventional self name, call the appropriate base initializer when extending a class, and keep unrelated work out of an operation callers expect to be predictable.