Python 3 — OOP: Inheritance & Dunder Methods

🐍 Python 3 🟢 Lesson 15 📅 July 2026

Inheritance is one of the most powerful features of OOP. It allows a class to inherit the attributes and methods of another class, promoting code reuse and building logical hierarchies. Dunder (double underscore) methods let you customize how Python's built-in operations behave for your custom classes.

1 Basic Inheritance
Python 3 — Inheritance▶ Run Code
# Parent class (Base class)
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
        self.is_alive = True

    def eat(self):
        print(f"{self.name} is eating.")

    def sleep(self):
        print(f"{self.name} is sleeping. Zzzz...")

    def __str__(self):
        return f"{self.name} ({self.species})"

# Child class inherits from Animal
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Canis familiaris")  # Call parent's __init__
        self.breed = breed

    # New method — only for Dog
    def fetch(self, item="ball"):
        print(f"{self.name} fetches the {item}!")

    # Overriding parent method
    def __str__(self):
        return f"{self.name} the {self.breed}"

class Cat(Animal):
    def __init__(self, name, indoor=True):
        super().__init__(name, "Felis catus")
        self.indoor = indoor

    def purr(self):
        print(f"{self.name}: Purrrrr...")

# Creating instances
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers")

dog.eat()       # Inherited from Animal
dog.fetch()     # Dog-specific
cat.sleep()     # Inherited from Animal
cat.purr()      # Cat-specific
print(dog)      # Uses Dog's __str__
2 Method Overriding & super()
Python 3 — Method Override▶ Run Code
class Shape:
    def __init__(self, color="white"):
        self.color = color

    def area(self):
        return 0  # Default implementation

    def describe(self):
        return f"A {self.color} {type(self).__name__} with area {self.area():.2f}"

class Rectangle(Shape):
    def __init__(self, width, height, color="blue"):
        super().__init__(color)
        self.width = width
        self.height = height

    def area(self):               # Override parent's area()
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius, color="red"):
        super().__init__(color)
        self.radius = radius

    def area(self):               # Override parent's area()
        import math
        return math.pi * self.radius ** 2

r = Rectangle(5, 10)
c = Circle(7)
print(r.describe())    # A blue Rectangle with area 50.00
print(c.describe())    # A red Circle with area 153.94
3 Polymorphism

Polymorphism means different classes can be used interchangeably as long as they share the same interface (methods):

Python 3 — Polymorphism▶ Run Code
class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Duck:
    def speak(self):
        return "Quack!"

# Polymorphism in action — same interface, different behavior
animals = [Dog(), Cat(), Duck(), Dog(), Cat()]

for animal in animals:
    # Each calls its own speak() — Python figures it out!
    print(f"{type(animal).__name__}: {animal.speak()}")

# Function that works with any animal
def make_noise(animal):
    print(f"The {type(animal).__name__} says: {animal.speak()}")

make_noise(Dog())
make_noise(Cat())
4 Multiple Inheritance & MRO
Python 3 — Multiple Inheritance▶ Run Code
class Flyable:
    def fly(self):
        return f"{self.__class__.__name__} is flying!"

class Swimmable:
    def swim(self):
        return f"{self.__class__.__name__} is swimming!"

class Walkable:
    def walk(self):
        return f"{self.__class__.__name__} is walking!"

class Duck(Flyable, Swimmable, Walkable):
    def quack(self):
        return "Quack!"

class FlyingFish(Flyable, Swimmable):
    pass

donald = Duck()
print(donald.fly())    # Duck is flying!
print(donald.swim())   # Duck is swimming!
print(donald.walk())   # Duck is walking!
print(donald.quack())  # Quack!

# MRO — Method Resolution Order
print(Duck.__mro__)    # Order Python searches for methods
5 Dunder (Magic) Methods

Dunder methods let your classes work with Python's built-in operators and functions:

MethodTriggered by
__str__print(obj), str(obj)
__repr__repr(obj), REPL display
__len__len(obj)
__add__obj1 + obj2
__eq__obj1 == obj2
__lt__obj1 < obj2
__getitem__obj[key]
__contains__item in obj
__iter__for item in obj
Python 3 — Dunder Methods▶ Run Code
class Vector:
    """2D Vector class with operator overloading."""
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

    def __repr__(self):
        return f"Vector(x={self.x}, y={self.y})"

    def __add__(self, other):    # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):    # v1 - v2
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):   # v * number
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other):     # v1 == v2
        return self.x == other.x and self.y == other.y

    def __abs__(self):           # abs(v) — magnitude
        return (self.x**2 + self.y**2) ** 0.5

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)        # Vector(4, 6)
print(v1 - v2)        # Vector(2, 2)
print(v1 * 3)         # Vector(9, 12)
print(v1 == v2)       # False
print(abs(v1))        # 5.0 (Pythagorean theorem)
6 Abstract Classes
Python 3 — Abstract Classes▶ Run Code
from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    """Abstract base class for payment processors."""

    @abstractmethod
    def process_payment(self, amount):
        """All subclasses MUST implement this."""
        pass

    @abstractmethod
    def refund(self, amount, transaction_id):
        pass

    def get_fee(self, amount):
        """Concrete method — shared by all processors."""
        return amount * 0.02

class StripeProcessor(PaymentProcessor):
    def process_payment(self, amount):
        fee = self.get_fee(amount)
        print(f"Stripe: Processing ₹{amount} (fee: ₹{fee:.2f})")
        return "TXN_STRIPE_001"

    def refund(self, amount, txn_id):
        print(f"Stripe: Refunding ₹{amount} for {txn_id}")

# Can't instantiate abstract class:
# p = PaymentProcessor()  # TypeError!

stripe = StripeProcessor()
txn = stripe.process_payment(1000)
stripe.refund(200, txn)
7 isinstance() & issubclass()
Python 3 — isinstance & issubclass▶ Run Code
class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass

d = Dog()
c = Cat()

# isinstance — checks object type
print(isinstance(d, Dog))      # True
print(isinstance(d, Animal))   # True (Dog IS an Animal)
print(isinstance(d, Cat))      # False

# issubclass — checks class hierarchy
print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Dog))    # False

# Practical use in functions
def make_sound(animal):
    if isinstance(animal, Dog):
        print("Woof!")
    elif isinstance(animal, Cat):
        print("Meow!")
    else:
        print("...")
8 Coding Challenge

Build a shape hierarchy with operator overloading:

  • Abstract base Shape with abstract area() and perimeter()
  • Concrete classes: Rectangle, Circle, Triangle
  • Each class should implement __str__, __eq__ (same area), and __lt__ (smaller area)
  • Create a list of mixed shapes, sort them by area using sorted()
  • Use isinstance() to count how many of each type exist
  • Find the shape with the largest area using max()