๐ Python OOP โ Simple Tutorial for Beginners
We'll cover:
- ๐งฑ Classes, Inheritance & MRO
- โจ Dunder / Magic Methods
- ๐ท๏ธ
@dataclass+ Abstract Base Classes (abc) - ๐งฉ Composition vs Inheritance
- ๐ Dunder Methods Cheat Sheet
1๏ธโฃ Classes, Inheritance & MRO
Let's start from the absolute basics.
๐งฑ What is a Class?
A class is like a blueprint for creating objects.
Imagine you want to create students.
Every student might have:
- name
- age
- college
Instead of writing separate code for every student, we create a class.
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print("Hi, I am", self.name)
Now we can create objects:
student1 = Student("Deepika", 21)
student2 = Student("Sneha", 22)
student1.introduce()
student2.introduce()
Output:
Hi, I am Deepika
Hi, I am Sneha
๐ง Think like this:
Class
โ
Blueprint
Object
โ
Actual thing created from blueprint
Example:
Student class
โ
โโโโโโโโโโโโโโโโโ
โ Deepika โ โ object
โ Sneha โ โ object
โ Koushik โ โ object
โโโโโโโโโโโโโโโโโ
2๏ธโฃ ๐จโ๐ฉโ๐ง What is Inheritance?
Inheritance means:
One class can reuse the properties and methods of another class.
Suppose we have:
class Animal:
def eat(self):
print("Animal is eating")
def sleep(self):
print("Animal is sleeping")
Now we create:
class Dog(Animal):
pass
Because Dog inherits from Animal, Dog automatically gets:
eat()sleep()
So:
dog = Dog()
dog.eat()
dog.sleep()
Output:
Animal is eating
Animal is sleeping
๐ We didn't write eat() or sleep() inside Dog.
That's inheritance.
๐ณ Parent and Child
We usually call:
Animal
โ
Parent class / Base class / Superclass
and:
Dog
โ
Child class / Derived class / Subclass
So:
class Dog(Animal):
means:
"Dog is a type of Animal."
3๏ธโฃ ๐ Method Overriding
What if Dog wants its own version of eat()?
Easy!
class Animal:
def eat(self):
print("Animal is eating")
class Dog(Animal):
def eat(self):
print("Dog is eating")
Now:
dog = Dog()
dog.eat()
Output:
Dog is eating
Python chooses the method from Dog.
This is called method overriding.
4๏ธโฃ ๐งญ What is MRO?
Now comes the scary-looking word:
MRO = Method Resolution Order
But don't worry ๐ญ.
It's simply:
The order in which Python searches for a method.
Suppose:
class Animal:
def speak(self):
print("Animal")
class Dog(Animal):
pass
When we do:
dog = Dog()
dog.speak()
Python asks:
Does Dog have speak()?
โ
NO
โ
Does Animal have speak()?
โ
YES
โ
Run Animal.speak()
That's basically method resolution.
๐ How do I see MRO?
Use:
print(Dog.mro())
or:
print(Dog.__mro__)
You'll get something similar to:
[<class 'Dog'>, <class 'Animal'>, <class 'object'>]
Meaning:
DogAnimalobject
Python searches in that order.
5๏ธโฃ ๐ต Multiple Inheritance + MRO
Here's where MRO becomes REALLY important.
class A:
def show(self):
print("A")
class B(A):
def show(self):
print("B")
class C(A):
def show(self):
print("C")
class D(B, C):
pass
Now:
obj = D()
obj.show()
What happens?
Python needs to decide:
D
โ
B or C?
โ
A?
Python uses MRO to decide the order.
print(D.mro())
Conceptually:
D โ B โ C โ A โ object
Therefore:
obj.show()
finds show() in B first.
Output:
B
โญ Important: You don't manually decide which parent's method runs. Python follows its MRO rules.
6๏ธโฃ โจ Dunder / Magic Methods
Now let's enter the fun part.
You've probably seen things like:
__init__
__str__
__repr__
__eq__
__len__
__add__
These are called Dunder methods.
"Dunder" = Double UNDERscore
__method__
โ โ
double double
underscore
They are also called magic methods.
7๏ธโฃ ๐ค Why do we need Dunder Methods?
Let's start with something familiar.
You can do:
a = 10
b = 20
print(a + b)
Output:
30
But what if we create our own class?
class Number:
def __init__(self, value):
self.value = value
Then:
a = Number(10)
b = Number(20)
print(a + b)
โ Python doesn't automatically know how to add two Number objects.
That's where __add__() comes in.
8๏ธโฃ __init__() ๐๏ธ
You've already seen this.
class Student:
def __init__(self, name):
self.name = name
When you write:
student = Student("Deepika")
Python automatically calls __init__().
Think:
Student("Deepika")
โ
__init__("Deepika")
โ
self.name = "Deepika"
๐ง Purpose: __init__() is used to initialize an object's data.
9๏ธโฃ __repr__() ๐
Suppose:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
Then:
student = Student("Deepika", 21)
print(student)
Without defining a representation, Python gives something ugly like:
<__main__.Student object at 0x000001...>
Not very useful ๐ญ.
So:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Student(name='{self.name}', age={self.age})"
Now:
print(student)
can show:
Student(name='Deepika', age=21)
๐ง Purpose: __repr__() gives a useful representation of an object, especially for debugging.
๐ __eq__() โ๏ธ
eq means equal.
Normally:
a == b
asks: "Are these two things equal?"
For our own class, we can define what equality means.
class Student:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
Now:
s1 = Student("Deepika")
s2 = Student("Deepika")
print(s1 == s2)
Output:
True
Because:
s1.name == s2.name
Deepika == Deepika
True
1๏ธโฃ1๏ธโฃ __len__() ๐
Python's len() can work with our class if we define __len__().
Example:
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
Now:
team = Team(["Deepika", "Sneha", "Koushik"])
print(len(team))
Output:
3
Python internally does something conceptually like:
team.__len__()
1๏ธโฃ2๏ธโฃ __add__() โ
This controls the + operator.
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Number(self.value + other.value)
Now:
a = Number(10)
b = Number(20)
c = a + b
print(c.value)
Output:
30
Behind the scenes:
a + b
roughly means:
a.__add__(b)
๐ฅ That's the magic.
1๏ธโฃ3๏ธโฃ Other Useful Dunder Methods
There are MANY, but you don't need to memorize all of them.
Here's the important beginner set:
| Dunder | Used for |
|---|---|
__init__ |
Initialize object |
__repr__ |
Developer-friendly representation |
__str__ |
User-friendly string |
__eq__ |
== |
__ne__ |
!= |
__lt__ |
< |
__gt__ |
> |
__le__ |
<= |
__ge__ |
>= |
__len__ |
len() |
__add__ |
+ |
__sub__ |
- |
__mul__ |
* |
__truediv__ |
/ |
__getitem__ |
obj[index] |
__setitem__ |
obj[index] = value |
__contains__ |
x in obj |
__iter__ |
iteration |
__next__ |
next item |
__enter__ |
with block starts |
__exit__ |
with block ends |
๐ท๏ธ 1๏ธโฃ4๏ธโฃ What is @dataclass?
Now imagine you're writing this:
class Student:
def __init__(self, name, age, college):
self.name = name
self.age = age
self.college = college
And then you also want __repr__ and __eq__.
That's a lot of repetitive code ๐ญ.
Python gives us @dataclass, which can automatically generate common methods for us.
Without @dataclass
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Student(name='{self.name}', age={self.age})"
def __eq__(self, other):
return self.name == other.name and self.age == other.age
That's a lot.
With @dataclass
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
That's it! ๐ญโค๏ธ
Now:
s1 = Student("Deepika", 21)
s2 = Student("Deepika", 21)
print(s1)
print(s1 == s2)
Output:
Student(name='Deepika', age=21)
True
๐ง Simple definition: @dataclass is a decorator that automatically creates common methods for classes that mainly store data.
It can generate things like:
__init____repr____eq__
and more depending on the options you use.
๐จ When should you use @dataclass?
Use it when your class is mainly a data container.
For example:
@dataclass
class Employee:
name: str
salary: float
department: str
Great use case โ .
But if your class contains complicated business logic and isn't primarily storing data, a normal class may be better.
๐ง 1๏ธโฃ5๏ธโฃ Abstract Base Classes โ abc
This sounds terrifying. It's actually pretty simple.
Imagine we say:
Every animal MUST have a
sound()method.
We don't want to provide the actual implementation in the parent class. We just want to create a rule.
That's where an abstract class comes in.
Example
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def sound(self):
pass
Now:
class Dog(Animal):
def sound(self):
print("Woof")
And:
class Cat(Animal):
def sound(self):
print("Meow")
Why is this useful?
We are basically saying:
Animal
โ
Every child MUST implement sound()
So:
dog = Dog()
dog.sound()
works.
But:
animal = Animal()
โ doesn't work because Animal is abstract.
๐ง Think of ABC like a contract
Imagine your manager says:
"Every employee class must have a
calculate_salary()method."
You can create:
class Employee(ABC):
@abstractmethod
def calculate_salary(self):
pass
Then:
class Developer(Employee):
def calculate_salary(self):
return 50000
class Designer(Employee):
def calculate_salary(self):
return 45000
The parent establishes the rule. The child provides the actual implementation.
๐งฉ 1๏ธโฃ6๏ธโฃ Composition vs Inheritance
This is SUPER important for interviews.
Let's first understand inheritance.
Inheritance = "IS-A"
Example:
Dog IS-A Animal
Car IS-A Vehicle
Manager IS-A Employee
So:
class Dog(Animal):
makes sense, because:
Dog โ Animal
IS-A relationship
๐งฉ Composition = "HAS-A"
Composition means one object contains/uses another object.
Example:
Car HAS-A Engine
House HAS-A Room
Computer HAS-A CPU
For example:
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine()
def start_car(self):
self.engine.start()
print("Car started")
Now:
car = Car()
car.start_car()
Output:
Engine started
Car started
Here:
Car
โ
HAS-A
โ
Engine
That's composition.
โ๏ธ Composition vs Inheritance
Let's compare.
Inheritance:
class Dog(Animal):
pass
Means: Dog IS-A Animal
Composition:
class Car:
def __init__(self):
self.engine = Engine()
Means: Car HAS-A Engine
๐ง When should I use which?
A simple rule:
Use inheritance when the relationship genuinely is IS-A:
Dog โ Animal
Cat โ Animal
Manager โ Employee
Use composition when the relationship is HAS-A:
Car โ Engine
Computer โ CPU
House โ Room
๐ก Why is composition often preferred?
Suppose:
class Car(Engine):
This says: "Car IS-A Engine."
๐คจ That doesn't make sense. A car isn't an engine. A car has an engine.
So:
class Car:
def __init__(self):
self.engine = Engine()
is much more logical.
๐ง The Entire Topic in One Picture
PYTHON OOP
โ
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ โ โ
Classes Inheritance Composition
โ โ โ
Blueprint IS-A HAS-A
โ โ โ
Objects Dog-Animal Car-Engine
โ
MRO
โ
Search order for methods
DUNDER METHODS
โ
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โ โ โ
__init__ __repr__ __eq__
โ โ โ
initialize representation equality
โ
โโโ __len__ โ len()
โโโ __add__ โ +
โโโ __str__ โ str()
โโโ __getitem__ โ []
โโโ __iter__ โ iteration
SPECIAL TOOLS
โ
โโโโโโโโโโโโโดโโโโโโโโโโโโ
โ โ
@dataclass abc
โ โ
Data-focused class Rules/contracts
auto-generated methods abstract methods
๐ Dunder Cheat Sheet โ SAVE THIS! โญ
| Method | What it controls | Easy meaning |
|---|---|---|
__init__ |
Object creation | Set up object |
__str__ |
str(obj) / print(obj)
|
User-friendly text |
__repr__ |
repr(obj) |
Developer representation |
__eq__ |
obj1 == obj2 |
Are they equal? |
__ne__ |
obj1 != obj2 |
Are they different? |
__lt__ |
obj1 < obj2 |
Less than |
__gt__ |
obj1 > obj2 |
Greater than |
__le__ |
obj1 <= obj2 |
Less/equal |
__ge__ |
obj1 >= obj2 |
Greater/equal |
__len__ |
len(obj) |
Object length |
__add__ |
obj1 + obj2 |
Addition |
__sub__ |
obj1 - obj2 |
Subtraction |
__mul__ |
obj1 * obj2 |
Multiplication |
__truediv__ |
obj1 / obj2 |
Division |
__getitem__ |
obj[key] |
Get item |
__setitem__ |
obj[key] = value |
Set item |
__contains__ |
x in obj |
Membership |
__iter__ |
for x in obj |
Make iterable |
__next__ |
next(obj) |
Get next item |
__enter__ |
Enter with
|
Start context manager |
__exit__ |
Exit with
|
Finish context manager |
__call__ |
obj() |
Make object callable |
๐ฏ The 10 Things I Want You to Remember
If you're preparing for your internship/interview, don't try to memorize everything at once.
Remember these:
- Class โ Blueprint for creating objects.
- Object โ Actual instance created from a class.
-
Inheritance โ Child class gets/reuses functionality from parent.
class Dog(Animal): - Method overriding โ Child provides its own version of parent's method.
-
MRO โ Order Python follows when searching for a method.
Dog.mro() -
Dunder methods โ Special methods that let our objects work naturally with Python operations.
__init__,__repr__,__eq__,__len__,__add__ -
@dataclassโ Makes data-focused classes much shorter by automatically generating common methods. -
Abstract Base Class โ Defines rules that child classes must follow.
@abstractmethod -
Inheritance = IS-A โ
Dog IS-A Animal -
Composition = HAS-A โ
Car HAS-A Engine
Top comments (0)