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):
- Python calls
Person.__new__(Person)to allocate a new, blank instance. - Python calls
Person.__init__(instance, "Ava", 30)on that instance to populate its attributes. - 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 asinstance.attribute1.- The method must implicitly return
None— noreturn valuestatement is allowed (a barereturnwith 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:
- Python looks up the
Personclass and callsPerson.__new__(Person). For ordinary classes inheriting fromobject, this allocates a new, empty instance in memory with no instance attributes yet. - Python calls
Person.__init__(new_instance, "Alice", 28). Inside,selfrefers tonew_instance, and the body runs top to bottom like any function, assigningself.nameandself.ageonto that instance's__dict__. - Python checks that
__init__returnedNone(it always should — falling off the end of the method counts as returningNone). - The fully initialized instance is returned from the
Person(...)call expression and bound to the namep.
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
Noneand 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_areaabove) 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
dataclasswhen 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
Bookwhose__init__takestitle,author, andpages, and raises aValueErrorifpagesis less than or equal to 0. Create one valid book and print its title and author. - Exercise 2: Write a class
Playlistwhose__init__takes anameand an optional list ofsongs(default should behave correctly for multiple independent playlists, avoiding the mutable-default pitfall). Add a methodadd_songthat appends to the list, and prove two separatePlaylistinstances don't share state. - Exercise 3: Write a class
Temperaturewhose__init__takes a value in Celsius and, inside__init__, computes and stores afahrenheitattribute using a helper method. Create an instance for 100 Celsius and print both attributes (expected:100and212.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 viaself.attr = value. __init__must returnNone— 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.
