In Python, self is the instance of the class itself — the specific object you are working with — and Python passes it automatically as the first argument to every instance method. __init__ is the method Python runs right after a new object is created so you can set up its starting attributes; it is an initializer, not a constructor. Together they answer two questions every class needs: which object am I touching? (self) and what state should it start with? (__init__).
This guide covers what self actually is, why Python makes it explicit, how __init__ differs from __new__, instance versus class attributes, default and keyword arguments, calling super().__init__() in inheritance, the most common mistakes, and how @dataclass removes most of the boilerplate. All examples are written for Python 3.12+.
A quick refresher: classes and objects
A class is a blueprint that describes the data (attributes) and behaviour (methods) shared by a category of things. An object (or instance) is one concrete thing built from that blueprint. One Car class can produce many car objects, each with its own colour and speed while sharing the same methods.
Think of it like a NFS game: the Car class defines attributes such as color, company, and speed_limit, and methods such as start, accelerate, and change_gear. Each car you spawn in the game is a separate instance of that one class.
What self really is
self is simply a reference to the instance the method is being called on. When you write car.start(), Python rewrites it behind the scenes as Car.start(car) — the object car is handed in as the first parameter, which we conventionally name self. Inside the method, self is your handle to that particular object's attributes and other methods.
Key points about self:
- It is passed implicitly when you call a method on an instance, but you declare it explicitly as the first parameter in the method definition.
- It is not a keyword —
selfis just a strong naming convention. You could call it something else, but you should not (more on that below). - It lets each instance keep its own state.
self.coloron one car is independent ofself.coloron another.
Why self is explicit in Python
Many languages (Java, C++, C#) have an implicit this. Python deliberately makes the instance explicit instead. The guiding principle is the Zen of Python's "Explicit is better than implicit." Making self a real parameter has practical benefits:
- It makes the rule uniform: a method is just a function whose first argument is the instance, so
Car.start(car)andcar.start()are obviously the same call. - It removes ambiguity between local variables and attributes —
self.color(attribute) is never confused withcolor(a local). - It makes advanced patterns (decorators, descriptors, passing unbound methods around) easy to reason about because the instance is right there in the signature.
__init__: the initializer
__init__ ("dunder init", for the double underscores) runs automatically when you create an object, letting you assign the instance's starting attributes. Here is the Car blueprint:
class Car:
"""Blueprint for a car."""
def __init__(self, model, color, company, speed_limit):
self.model = model
self.color = color
self.company = company
self.speed_limit = speed_limit
def start(self):
print(f"{self.company} {self.model} started")
def stop(self):
print("stopped")
def accelerate(self):
print("accelerating...")
def change_gear(self, gear_type):
print(f"gear changed to {gear_type}")Now create two different cars from the same class:
maruti_suzuki = Car("Ertiga", "black", "Suzuki", 60)
audi = Car("A6", "red", "Audi", 80)
maruti_suzuki.start() # Suzuki Ertiga started
audi.start() # Audi A6 startedWhen you write Car("Ertiga", "black", "Suzuki", 60), Python creates the object and immediately calls __init__, passing the new object as self and your four values as the remaining arguments. Each call binds those values onto a separate instance, so maruti_suzuki and audi never share state. You never write return in __init__ — it must return None; returning anything else raises a TypeError.
Is __init__ a constructor? __init__ vs __new__
Strictly speaking, __init__ is not a constructor — it is an initializer. The real construction happens in __new__, which actually allocates and returns the new, empty object. Python then calls __init__ on that already-created object to populate it.
The normal flow when you call Car(...) is:
Car.__new__(Car)creates and returns a blank instance.Car.__init__(instance, ...)initialises that instance's attributes.
You rarely override __new__ — it matters mainly for immutable types (subclassing int, str, tuple), singletons, or metaclass tricks. For everyday classes, __init__ is all you need. The takeaway: __init__ configures an object that already exists; it does not create it.
Instance attributes vs class attributes
Attributes assigned through self inside __init__ are instance attributes — unique to each object. Attributes defined directly in the class body are class attributes — shared by every instance unless an instance overrides its own copy.
class Car:
wheels = 4 # class attribute: shared by all cars
def __init__(self, model, color):
self.model = model # instance attribute: per object
self.color = color
a = Car("A6", "red")
b = Car("Ertiga", "black")
print(a.wheels, b.wheels) # 4 4 -> same class attribute
print(a.color, b.color) # red black -> independent instance attributes
Car.wheels = 6 # change the shared class attribute
print(a.wheels, b.wheels) # 6 6 -> both see the change
a.wheels = 8 # creates an instance attribute that shadows the class one
print(a.wheels, b.wheels) # 8 6Use class attributes for constants and defaults shared across all instances; use instance attributes (set via self in __init__) for anything that varies per object.
Default and keyword arguments in __init__
__init__ is an ordinary method, so it supports default values, keyword arguments, and *args/**kwargs. Defaults let callers omit values they do not need. This Rectangle example shows unit_cost defaulting to 0, and self being used to call one method from another:
class Rectangle:
def __init__(self, length, breadth, unit_cost=0):
self.length = length
self.breadth = breadth
self.unit_cost = unit_cost
def get_perimeter(self):
return 2 * (self.length + self.breadth)
def get_area(self):
return self.length * self.breadth
def calculate_cost(self):
return self.get_area() * self.unit_cost
# length = 160 cm, breadth = 120 cm, cost = 2000 per cm^2
r = Rectangle(160, 120, unit_cost=2000)
print(f"Area: {r.get_area()} cm^2") # Area: 19200 cm^2
print(f"Cost: Rs. {r.calculate_cost()}") # Cost: Rs. 38400000Notice how calculate_cost calls self.get_area(): inside the class you reach other methods through self, exactly as an outside caller reaches them through the variable r. self is r — the same object, just named from inside the class. If get_area needed extra arguments you would pass them after self; the instance is always supplied first, automatically.
Calling super().__init__() in inheritance
When a subclass defines its own __init__, it replaces the parent's. To still run the parent's initialisation, call super().__init__(...). This keeps the parent's setup intact while you add subclass-specific attributes.
class Vehicle:
def __init__(self, company, speed_limit):
self.company = company
self.speed_limit = speed_limit
class Car(Vehicle):
def __init__(self, company, speed_limit, model):
super().__init__(company, speed_limit) # run Vehicle's __init__
self.model = model # add Car-specific state
c = Car("Audi", 80, "A6")
print(c.company, c.speed_limit, c.model) # Audi 80 A6Always call super().__init__() first if the subclass relies on attributes the parent sets. The zero-argument super() form (no need to write super(Car, self)) has been the idiomatic style since Python 3 and cooperates correctly with multiple inheritance, following the method resolution order (MRO).
Common mistakes to avoid
1. Forgetting self in the method signature. Every instance method must declare self as its first parameter, or calling it on an instance raises a TypeError about too many arguments.
2. Forgetting self. when assigning attributes. Writing color = color inside __init__ creates a throwaway local variable; you need self.color = color to store it on the instance.
3. Mutable default arguments. A default like def __init__(self, items=[]) is evaluated once at definition time and shared across every instance, so appends leak between objects. Use None as the sentinel instead:
# WRONG: the same list is reused by every instance
class BasketBad:
def __init__(self, items=[]):
self.items = items
a = BasketBad()
a.items.append("apple")
b = BasketBad()
print(b.items) # ['apple'] -> leaked from a!
# RIGHT: create a fresh list per instance
class Basket:
def __init__(self, items=None):
self.items = items if items is not None else []
x = Basket()
x.items.append("apple")
y = Basket()
print(y.items) # [] -> independentA modern shortcut: @dataclass
For classes that mainly hold data, the dataclasses module (standard library since Python 3.7) writes __init__ — plus __repr__ and __eq__ — for you from the type-annotated fields. It is the idiomatic modern way to avoid repetitive self.x = x boilerplate, and it handles the mutable-default trap safely via field(default_factory=...).
from dataclasses import dataclass, field
@dataclass
class Car:
model: str
color: str
company: str
speed_limit: int = 60
extras: list[str] = field(default_factory=list) # safe mutable default
audi = Car("A6", "red", "Audi", 80)
print(audi) # Car(model='A6', color='red', company='Audi', speed_limit=80, extras=[])Behind the scenes @dataclass still generates an ordinary __init__ that uses self exactly as shown earlier — so understanding self and __init__ remains essential even when you let the decorator do the typing. Reach for a dataclass when a class is mostly fields; write __init__ by hand when initialisation needs real logic, validation, or computed attributes.
Where to go next
To deepen your object-oriented Python, read our introduction to object-oriented programming with Python 3, explore the full list of Python class special (magic/dunder) methods that includes __init__ and __new__, and see how Python properties let you add validation on top of plain attributes. To customise behaviour around methods, our guide to Python decorators is a natural follow-up.
At MicroPyramid we have built and maintained production Python applications for clients across the US, UK, Australia, and beyond for over a decade — if you need a team that writes idiomatic, well-structured Python, see our Python development services.
Frequently Asked Questions
Is __init__ a constructor?
Not strictly. __init__ is an initializer: it runs on an object that already exists to set its starting attributes. The actual construction (allocating the object) is done by __new__, which Python calls first. In casual conversation people often call __init__ "the constructor", but the precise answer is that __new__ creates and __init__ initialises.
What does self mean in Python?
self is the instance the method is operating on. When you call obj.method(), Python passes obj in as the first argument, which you name self. You use it to read and set that object's attributes (self.color) and to call its other methods (self.get_area()).
Can I rename self to something else?
Technically yes — self is a convention, not a keyword, so Python will accept any valid name as the first parameter. In practice you should always use self, because every Python developer, linter, and IDE expects it; renaming it only hurts readability.
Why do I have to write self explicitly?
Python favours being explicit over implicit. Declaring self as a real parameter makes methods just functions whose first argument is the instance, removes confusion between attributes and local variables, and keeps advanced features like decorators and descriptors easy to reason about.
What is the difference between instance and class attributes?
Instance attributes are set on self (usually in __init__) and are unique to each object. Class attributes are defined in the class body and shared by all instances unless an instance assigns its own value, which then shadows the shared one.
Why are mutable default arguments dangerous in __init__?
Default argument values are evaluated once when the function is defined, so a default like items=[] is a single list shared by every instance — mutating it on one object affects all others. Use items=None and create a fresh list inside __init__, or use field(default_factory=list) in a dataclass.