Python OOP: Classes, __init__ and self, Explained
A class is a blueprint for objects; __init__ sets up each new object; self refers to the object itself. Here's how object-oriented Python works — methods, attributes, inheritance and dunder methods — with clear examples.
In Python, a class is a blueprint for creating objects, __init__ is the method that sets up each new object, and self is how an object refers to itself. Together they let you bundle data and the functions that operate on it into one tidy unit — which is what object-oriented programming is.
OOP is how you organise anything bigger than a script, and classes/__init__/self are the first questions any Python interview asks. Here’s the whole model, built up piece by piece.
A class and its objects
A class defines a type of thing; an object (or instance) is one specific thing of that type. One Dog class, many dog objects.
class Dog:
def __init__(self, name, breed):
self.name = name # instance attributes
self.breed = breed
def bark(self): # a method
return f"{self.name} says woof!"
rex = Dog("Rex", "Lab") # create an instance
print(rex.bark()) # "Rex says woof!"
__init__ — the setup method
__init__ runs automatically every time you create an object. Its job is to give the new object its starting attributes. When you write Dog("Rex", "Lab"), Python creates a blank object and immediately calls __init__ with it, so self.name becomes "Rex".
It’s often called the constructor, though technically it initialises an object Python has already created.
self — the object referring to itself
self is the current object. Every instance method takes it as the first parameter, and it’s how the method reaches that object’s own data.
rex.bark()
# Python calls Dog.bark(rex) under the hood —
# so inside bark(), self IS rex.
You write self in the definition but never pass it explicitly — Python fills it in from whatever is before the dot.
self is not a keyword — it's just a strong convention. Python passes the instance as the first argument to every method automatically, and we name that parameter self. Forgetting it is the #1 beginner OOP error.
Instance vs class attributes
This distinction trips people up and shows up in interviews.
class Dog:
species = "Canis familiaris" # class attribute — shared by ALL dogs
def __init__(self, name):
self.name = name # instance attribute — unique per dog
species lives on the class and is the same for every instance; name lives on each object. Read class attributes for constants shared across all objects — and avoid mutable class attributes (a shared list is a classic bug; see below).
Inheritance — building on a base class
A class can extend another, inheriting its attributes and methods and adding or overriding its own.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Cat(Animal): # Cat IS an Animal
def speak(self): # override
return f"{self.name} says meow"
Cat("Milo").speak() # "Milo says meow"
Use super().__init__(...) to call the parent’s setup from a child. Inheritance models an “is-a” relationship — reach for it when a subtype genuinely is a kind of the base type, and prefer composition otherwise.
Dunder methods — making objects feel native
“Dunder” (double-underscore) methods let your objects plug into Python’s built-in behaviour:
class Money:
def __init__(self, cents):
self.cents = cents
def __repr__(self): # how it prints / shows in REPL
return f"Money({self.cents})"
def __eq__(self, other): # how == works
return self.cents == other.cents
def __add__(self, other): # how + works
return Money(self.cents + other.cents)
Now Money(150) + Money(50) works, and == compares by value. Defining __str__/__repr__ is the highest-leverage habit — it makes debugging far easier.
@property — computed attributes
A @property lets a method be accessed like an attribute, useful for derived or validated values:
class Circle:
def __init__(self, r):
self.r = r
@property
def area(self):
return 3.14159 * self.r ** 2
Circle(2).area # 12.566… — called without ()
@property is itself a decorator — once you know OOP, decorators are the natural next step.
Common mistakes
- Forgetting
selfin a method signature, or forgetting to use it to access attributes. - Mutable class attributes. A list defined at class level is shared by all instances — mutate it through one object and every object sees the change. Put per-object state in
__init__. - Overusing inheritance. Deep hierarchies are brittle; prefer composition (an object has another object) when there’s no true “is-a”.
- Skipping
__repr__, then wondering why your objects print as<Dog object at 0x...>.
class Cart: items = [] shares one list across every cart. Always initialise per-object state inside __init__ (self.items = []), never as a bare class attribute.
Where this fits
OOP is Phase 4 of our Python roadmap, built on the data structures you store in objects and leading into decorators (which @property and @classmethod are examples of).
Classes are introduced gently in Python in One Month and taken to job-ready depth — dunder methods, inheritance vs composition, design — in Python in Three Months. For dataclasses, __slots__, metaclasses, and the data model that powers Python’s object system, Python for Staff Engineers goes deeper.
Get classes, __init__, and self solid, and the rest of OOP is just variations on the theme.
Frequently asked questions
What is __init__ in Python?
__init__ is the initialiser (constructor) method that runs automatically when you create a new object from a class. It sets up the object's starting attributes. For example, def __init__(self, name): self.name = name stores the name on each new instance.
What does self mean in Python?
self is a reference to the current object — the specific instance a method is being called on. It is how a method reads and writes that object's own attributes. You write self as the first parameter of every instance method, but you do not pass it explicitly; Python supplies it.
What is the difference between a class attribute and an instance attribute?
An instance attribute is set per object (usually in __init__ with self.x) and is unique to that object. A class attribute is defined directly in the class body and shared by all instances. Use instance attributes for per-object data and class attributes for constants shared across all objects.
What are dunder methods in Python?
Dunder ('double underscore') methods like __init__, __str__, __repr__ and __eq__ are special methods Python calls automatically to hook into built-in behaviour — printing, comparing, adding. Defining them lets your objects behave like native types.