Python The __init__ Method

When you create an object from a class in Python, you usually want it to start out with some data already filled in — a new Person should have a name, a new BankAccount should have a balance. The __init__ method is the special method Python calls automatically right after an object is created, and it’s where you set up that initial state. Understanding __init__ deeply — not just how to write one, but what Python is actually doing when it calls it — is essential to writing correct, predictable classes.

Overview / How it works

__init__ stands for "initialize." It is one of Python’s dunder methods (double-underscore methods), also called magic methods, that Python invokes automatically at specific moments. __init__ is invoked automatically immediately after a new instance of a class has been created in memory, and its job is to set the instance’s initial attributes.

It’s important to understand that __init__ does not create the object — it initializes an object that already exists. Object creation is actually handled by a different dunder method, __new__, which runs first and returns a new, mostly-empty instance. Python then passes that instance into __init__ as the first argument (conventionally named self), along with whatever arguments you supplied when calling the class. In practice, this whole sequence happens automatically when you write Person("Ava", 30):

  1. Python calls Person.__new__(Person) to allocate a new, blank instance.
  2. Python calls Person.__init__(instance, "Ava", 30) on that instance to populate its attributes.
  3. The now-initialized instance is bound to whatever variable you assigned it to.

Because almost every class only needs to customize step 2, you’ll rarely write __new__ yourself — __init__ is the workhorse for setup in everyday Python code. Inside __init__, self is a reference to the specific instance being built. Any attribute you assign with self.attribute_name = value becomes part of that instance’s own namespace (technically stored in the instance’s __dict__), completely separate from the same attribute on any other instance of the class.

__init__ is also different from a constructor in languages like Java or C++ in one key way: it must not return anything other than None. If you try to return a value from __init__, Python raises a TypeError at call time, because __init__‘s job is to configure the object, not produce a value.

Syntax

class ClassName:
    def __init__(self, param1, param2=default_value):
        self.attribute1 = param1
        self.attribute2 = param2
  • self — the instance being initialized. Always the first parameter; Python supplies it automatically, you never pass it explicitly.
  • param1, param2 — ordinary parameters, just like any function. They can have defaults, use *args/**kwargs, and be positional-only or keyword-only.
  • self.attribute1 = param1 — binds a value to the instance, making it accessible later as instance.attribute1.
  • The method must implicitly return None — no return value statement is allowed (a bare return with no value is fine).

Examples

Example 1: A basic class with __init__

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

    def introduce(self):
        return f"Hi, I'm {self.name} and I'm {self.age} years old."


alice = Person("Alice", 28)
bob = Person("Bob", 35)

print(alice.introduce())
print(bob.introduce())
print(alice.name, bob.name)
Output:
Hi, I'm Alice and I'm 28 years old.
Hi, I'm Bob and I'm 35 years old.
Alice Bob

Each call to Person(...) triggers __init__ with a fresh self. alice and bob each get their own independent name and age attributes, stored separately, even though they came from the same __init__ code.

Example 2: Default arguments and validation

class BankAccount:
    def __init__(self, owner, balance=0.0):
        if balance < 0:
            raise ValueError("Initial balance cannot be negative")
        self.owner = owner
        self.balance = balance
        self.transaction_log = []

    def deposit(self, amount):
        self.balance += amount
        self.transaction_log.append(f"Deposited {amount}")


acc1 = BankAccount("Priya")
acc2 = BankAccount("Sam", balance=150.0)

acc1.deposit(50)

print(acc1.balance, acc1.transaction_log)
print(acc2.balance, acc2.transaction_log)
Output:
50 ['Deposited 50']
150.0 []

This example shows two important patterns: a default argument (balance=0.0) so callers can omit it, and validation inside __init__ that raises an exception before any attribute is set, preventing an invalid object from ever being usable. Notice also that self.transaction_log = [] creates a brand-new empty list for every instance — each account gets its own log, not a shared one.

Example 3: __init__ calling other methods and using type hints

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height
        self.area = self._compute_area()

    def _compute_area(self) -> float:
        return self.width * self.height

    def __repr__(self) -> str:
        return f"Rectangle(width={self.width}, height={self.height})"


rects = [Rectangle(3, 4), Rectangle(2.5, 6)]
for r in rects:
    print(r, "-> area:", r.area)
Output:
Rectangle(width=3, height=4) -> area: 12
Rectangle(width=2.5, height=6) -> area: 15.0

__init__ isn't limited to plain assignments — it can call other methods on self as part of setup, as long as those methods only depend on attributes already assigned. Here _compute_area is called after width and height are set, so it can safely use them. Note the mixed output types: 3 * 4 stays an int (12), while 2.5 * 6 produces a float (15.0), exactly matching Python's normal numeric promotion rules.

Under the hood: step by step

When you write p = Person("Alice", 28), here is precisely what Python does:

  1. Python looks up the Person class and calls Person.__new__(Person). For ordinary classes inheriting from object, this allocates a new, empty instance in memory with no instance attributes yet.
  2. Python calls Person.__init__(new_instance, "Alice", 28). Inside, self refers to new_instance, and the body runs top to bottom like any function, assigning self.name and self.age onto that instance's __dict__.
  3. Python checks that __init__ returned None (it always should — falling off the end of the method counts as returning None).
  4. The fully initialized instance is returned from the Person(...) call expression and bound to the name p.

A useful mental model: ClassName(...) is really sugar for "call __new__ to get a blank object, then call __init__ on it to fill it in, then hand it back to you." Because self.attribute = value writes directly to the instance (not the class), attributes set in __init__ are independent per object — this is why two instances of the same class never accidentally share mutable state, as long as that state is created fresh inside __init__ (see Common Mistakes below for the case where this goes wrong).

Common Mistakes

Mistake 1: Using a mutable default argument

A classic and dangerous bug is giving a parameter a mutable default like a list or dict:

class ShoppingCart:
    def __init__(self, items=[]):  # BUG: shared default list
        self.items = items

Default argument values in Python are evaluated once, when the function is defined — not each time it's called. So every ShoppingCart created without an explicit items argument shares the exact same list object. Appending to one cart's items would silently appear in every other cart's items too. The fix is to default to None and create a new list inside the method:

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

Mistake 2: Forgetting self, or trying to return a value

Two related errors trip up beginners. First, forgetting self as the first parameter:

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

Here Python still passes the instance as the first positional argument, so it silently gets bound to what you named x, and the real x argument the caller passed is lost or raises a TypeError about argument count. Always name the first parameter self by convention. Second, returning a value from __init__:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        return self  # TypeError: __init__() should return None

Calling Point(1, 2) with that code raises TypeError: __init__() should return None. Just remove the return self line — self is already returned to the caller automatically by the class call mechanism.

Best Practices

  • Always name the first parameter self — it's a universal Python convention, not a keyword, but breaking it confuses every reader of your code.
  • Validate arguments early in __init__ and raise a clear exception (ValueError, TypeError) before assigning any attributes, so you never end up with a half-valid object.
  • Never use a mutable object (list, dict, set) as a default argument value; use None and create the mutable object inside the method body.
  • Use type hints on parameters (e.g. name: str, age: int) to make the expected shape of your class clear to readers and to editors/type checkers.
  • Keep __init__ focused on assignment and light validation; if it needs heavy computation, factor that into a small private helper method (as with _compute_area above) or a classmethod-based alternate constructor.
  • For classes with many optional fields, prefer keyword arguments with sensible defaults over long positional parameter lists, or consider a dataclass when you mainly need simple attribute storage.
  • Don't perform side effects with lasting external impact (network calls, file writes) inside __init__ — keep object construction cheap and predictable.

Practice Exercises

  • Exercise 1: Write a class Book whose __init__ takes title, author, and pages, and raises a ValueError if pages is less than or equal to 0. Create one valid book and print its title and author.
  • Exercise 2: Write a class Playlist whose __init__ takes a name and an optional list of songs (default should behave correctly for multiple independent playlists, avoiding the mutable-default pitfall). Add a method add_song that appends to the list, and prove two separate Playlist instances don't share state.
  • Exercise 3: Write a class Temperature whose __init__ takes a value in Celsius and, inside __init__, computes and stores a fahrenheit attribute using a helper method. Create an instance for 100 Celsius and print both attributes (expected: 100 and 212.0).

Summary

  • __init__ is the initializer method Python calls automatically right after a new instance is created by __new__.
  • It receives self (the new instance) plus any arguments passed when the class was called, and its job is to assign initial attributes via self.attr = value.
  • __init__ must return None — it initializes the object, it doesn't create or return it.
  • Attributes assigned in __init__ live on the instance, so each object gets independent state.
  • Avoid mutable default arguments; validate inputs early; keep __init__ lightweight and predictable.