DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Week-04,Task- 1: OOP & Dunder Methods

๐Ÿ 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)
Enter fullscreen mode Exit fullscreen mode

Now we can create objects:

student1 = Student("Deepika", 21)
student2 = Student("Sneha", 22)

student1.introduce()
student2.introduce()
Enter fullscreen mode Exit fullscreen mode

Output:

Hi, I am Deepika
Hi, I am Sneha
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  Think like this:

Class
  โ†“
Blueprint

Object
  โ†“
Actual thing created from blueprint
Enter fullscreen mode Exit fullscreen mode

Example:

Student class
     โ†“
 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 โ”‚ Deepika       โ”‚ โ† object
 โ”‚ Sneha         โ”‚ โ† object
 โ”‚ Koushik       โ”‚ โ† object
 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Now we create:

class Dog(Animal):
    pass
Enter fullscreen mode Exit fullscreen mode

Because Dog inherits from Animal, Dog automatically gets:

  • eat()
  • sleep()

So:

dog = Dog()

dog.eat()
dog.sleep()
Enter fullscreen mode Exit fullscreen mode

Output:

Animal is eating
Animal is sleeping
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ‰ We didn't write eat() or sleep() inside Dog.

That's inheritance.

๐ŸŒณ Parent and Child

We usually call:

Animal
   โ†“
Parent class / Base class / Superclass
Enter fullscreen mode Exit fullscreen mode

and:

Dog
   โ†“
Child class / Derived class / Subclass
Enter fullscreen mode Exit fullscreen mode

So:

class Dog(Animal):
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Now:

dog = Dog()
dog.eat()
Enter fullscreen mode Exit fullscreen mode

Output:

Dog is eating
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

When we do:

dog = Dog()
dog.speak()
Enter fullscreen mode Exit fullscreen mode

Python asks:

Does Dog have speak()?
        โ†“
       NO
        โ†“
Does Animal have speak()?
        โ†“
       YES
        โ†“
Run Animal.speak()
Enter fullscreen mode Exit fullscreen mode

That's basically method resolution.

๐Ÿ” How do I see MRO?

Use:

print(Dog.mro())
Enter fullscreen mode Exit fullscreen mode

or:

print(Dog.__mro__)
Enter fullscreen mode Exit fullscreen mode

You'll get something similar to:

[<class 'Dog'>, <class 'Animal'>, <class 'object'>]
Enter fullscreen mode Exit fullscreen mode

Meaning:

  1. Dog
  2. Animal
  3. object

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
Enter fullscreen mode Exit fullscreen mode

Now:

obj = D()
obj.show()
Enter fullscreen mode Exit fullscreen mode

What happens?

Python needs to decide:

D
โ†“
B or C?
โ†“
A?
Enter fullscreen mode Exit fullscreen mode

Python uses MRO to decide the order.

print(D.mro())
Enter fullscreen mode Exit fullscreen mode

Conceptually:

D โ†’ B โ†’ C โ†’ A โ†’ object
Enter fullscreen mode Exit fullscreen mode

Therefore:

obj.show()
Enter fullscreen mode Exit fullscreen mode

finds show() in B first.

Output:

B
Enter fullscreen mode Exit fullscreen mode

โญ 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__
Enter fullscreen mode Exit fullscreen mode

These are called Dunder methods.

"Dunder" = Double UNDERscore

__method__
โ†‘       โ†‘
double  double
underscore
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

30
Enter fullscreen mode Exit fullscreen mode

But what if we create our own class?

class Number:

    def __init__(self, value):
        self.value = value
Enter fullscreen mode Exit fullscreen mode

Then:

a = Number(10)
b = Number(20)

print(a + b)
Enter fullscreen mode Exit fullscreen mode

โŒ 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
Enter fullscreen mode Exit fullscreen mode

When you write:

student = Student("Deepika")
Enter fullscreen mode Exit fullscreen mode

Python automatically calls __init__().

Think:

Student("Deepika")
       โ†“
__init__("Deepika")
       โ†“
self.name = "Deepika"
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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
Enter fullscreen mode Exit fullscreen mode

Then:

student = Student("Deepika", 21)

print(student)
Enter fullscreen mode Exit fullscreen mode

Without defining a representation, Python gives something ugly like:

<__main__.Student object at 0x000001...>
Enter fullscreen mode Exit fullscreen mode

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})"
Enter fullscreen mode Exit fullscreen mode

Now:

print(student)
Enter fullscreen mode Exit fullscreen mode

can show:

Student(name='Deepika', age=21)
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  Purpose: __repr__() gives a useful representation of an object, especially for debugging.


๐Ÿ”Ÿ __eq__() โš–๏ธ

eq means equal.

Normally:

a == b
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now:

s1 = Student("Deepika")
s2 = Student("Deepika")

print(s1 == s2)
Enter fullscreen mode Exit fullscreen mode

Output:

True
Enter fullscreen mode Exit fullscreen mode

Because:

s1.name == s2.name
Deepika == Deepika
True
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Now:

team = Team(["Deepika", "Sneha", "Koushik"])

print(len(team))
Enter fullscreen mode Exit fullscreen mode

Output:

3
Enter fullscreen mode Exit fullscreen mode

Python internally does something conceptually like:

team.__len__()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Now:

a = Number(10)
b = Number(20)

c = a + b

print(c.value)
Enter fullscreen mode Exit fullscreen mode

Output:

30
Enter fullscreen mode Exit fullscreen mode

Behind the scenes:

a + b
Enter fullscreen mode Exit fullscreen mode

roughly means:

a.__add__(b)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ฅ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That's a lot.

With @dataclass

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    age: int
Enter fullscreen mode Exit fullscreen mode

That's it! ๐Ÿ˜ญโค๏ธ

Now:

s1 = Student("Deepika", 21)
s2 = Student("Deepika", 21)

print(s1)
print(s1 == s2)
Enter fullscreen mode Exit fullscreen mode

Output:

Student(name='Deepika', age=21)
True
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now:

class Dog(Animal):

    def sound(self):
        print("Woof")
Enter fullscreen mode Exit fullscreen mode

And:

class Cat(Animal):

    def sound(self):
        print("Meow")
Enter fullscreen mode Exit fullscreen mode

Why is this useful?

We are basically saying:

Animal
  โ†“
Every child MUST implement sound()
Enter fullscreen mode Exit fullscreen mode

So:

dog = Dog()
dog.sound()
Enter fullscreen mode Exit fullscreen mode

works.

But:

animal = Animal()
Enter fullscreen mode Exit fullscreen mode

โŒ 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
Enter fullscreen mode Exit fullscreen mode

Then:

class Developer(Employee):

    def calculate_salary(self):
        return 50000


class Designer(Employee):

    def calculate_salary(self):
        return 45000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So:

class Dog(Animal):
Enter fullscreen mode Exit fullscreen mode

makes sense, because:

Dog โ†’ Animal
IS-A relationship
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ Composition = "HAS-A"

Composition means one object contains/uses another object.

Example:

Car HAS-A Engine
House HAS-A Room
Computer HAS-A CPU
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Now:

car = Car()
car.start_car()
Enter fullscreen mode Exit fullscreen mode

Output:

Engine started
Car started
Enter fullscreen mode Exit fullscreen mode

Here:

Car
 โ†“
HAS-A
 โ†“
Engine
Enter fullscreen mode Exit fullscreen mode

That's composition.

โš”๏ธ Composition vs Inheritance

Let's compare.

Inheritance:

class Dog(Animal):
    pass
Enter fullscreen mode Exit fullscreen mode

Means: Dog IS-A Animal

Composition:

class Car:

    def __init__(self):
        self.engine = Engine()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Use composition when the relationship is HAS-A:

Car โ†’ Engine
Computer โ†’ CPU
House โ†’ Room
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Why is composition often preferred?

Suppose:

class Car(Engine):
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“‹ 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:

  1. Class โ€” Blueprint for creating objects.
  2. Object โ€” Actual instance created from a class.
  3. Inheritance โ€” Child class gets/reuses functionality from parent. class Dog(Animal):
  4. Method overriding โ€” Child provides its own version of parent's method.
  5. MRO โ€” Order Python follows when searching for a method. Dog.mro()
  6. Dunder methods โ€” Special methods that let our objects work naturally with Python operations. __init__, __repr__, __eq__, __len__, __add__
  7. @dataclass โ€” Makes data-focused classes much shorter by automatically generating common methods.
  8. Abstract Base Class โ€” Defines rules that child classes must follow. @abstractmethod
  9. Inheritance = IS-A โ€” Dog IS-A Animal
  10. Composition = HAS-A โ€” Car HAS-A Engine

Top comments (0)