All Posts
pythonPart 7 of python-basics-to-advanced

Python OOP #2 — Inheritance, ABCs & Duck Typing

Single and multiple inheritance, abstract base classes, mixins, Python's Protocol type, and why duck typing changes how you think about interfaces.

R
by Rupa
Apr 13, 20255 min read

Inheritance

animals.py
Loading editor...

Calling the Parent

class Vehicle:
    def __init__(self, make: str, model: str, year: int):
        self.make  = make
        self.model = model
        self.year  = year

    def describe(self) -> str:
        return f"{self.year} {self.make} {self.model}"

class ElectricCar(Vehicle):
    def __init__(self, make: str, model: str, year: int, range_km: int):
        super().__init__(make, model, year)   # call parent __init__
        self.range_km = range_km

    def describe(self) -> str:
        base = super().describe()             # extend parent method
        return f"{base} (EV, {self.range_km}km range)"

tesla = ElectricCar("Tesla", "Model 3", 2024, 570)
print(tesla.describe())
# 2024 Tesla Model 3 (EV, 570km range)

print(isinstance(tesla, ElectricCar))  # True
print(isinstance(tesla, Vehicle))      # True — it IS a Vehicle
print(issubclass(ElectricCar, Vehicle)) # True

Abstract Base Classes

ABCs define a required interface — subclasses must implement abstract methods:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """Calculate the area of the shape."""
        ...

    @abstractmethod
    def perimeter(self) -> float:
        """Calculate the perimeter of the shape."""
        ...

    def describe(self) -> str:
        """Concrete method — shared by all shapes"""
        return (f"{type(self).__name__}: "
                f"area={self.area():.2f}, "
                f"perimeter={self.perimeter():.2f}")

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self)      -> float: return 3.14159 * self.radius ** 2
    def perimeter(self) -> float: return 2 * 3.14159 * self.radius

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width  = width
        self.height = height

    def area(self)      -> float: return self.width * self.height
    def perimeter(self) -> float: return 2 * (self.width + self.height)

shapes: list[Shape] = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.describe())

# Shape()  # ❌ TypeError: Can't instantiate abstract class
ABC vs regular class

Use an ABC when you want to enforce a contract — subclasses that miss an abstract method raise a TypeError at instantiation, not at the call site. It catches bugs early.

Multiple Inheritance and Mixins

Python supports multiple inheritance. Mixins are small, focused classes that add one behaviour:

class LogMixin:
    """Adds logging capability to any class"""
    def log(self, message: str) -> None:
        print(f"[{type(self).__name__}] {message}")

class ValidateMixin:
    """Adds validation helpers"""
    def validate_positive(self, value: float, name: str) -> None:
        if value <= 0:
            raise ValueError(f"{name} must be positive, got {value}")

class BankAccount(LogMixin, ValidateMixin):
    def __init__(self, owner: str, balance: float = 0):
        self.owner = owner
        self.validate_positive(balance + 1, "initial balance")  # allow 0
        self._balance = balance

    def deposit(self, amount: float) -> None:
        self.validate_positive(amount, "deposit amount")
        self._balance += amount
        self.log(f"Deposited ${amount:.2f}. Balance: ${self._balance:.2f}")

    def withdraw(self, amount: float) -> None:
        self.validate_positive(amount, "withdrawal amount")
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        self.log(f"Withdrew ${amount:.2f}. Balance: ${self._balance:.2f}")

acc = BankAccount("Rupa", 1000)
acc.deposit(500)     # [BankAccount] Deposited $500.00. Balance: $1500.00
acc.withdraw(200)    # [BankAccount] Withdrew $200.00. Balance: $1300.00

Method Resolution Order (MRO)

Python uses C3 linearisation to resolve which method to call in multiple inheritance:

class A:
    def hello(self): print("A")

class B(A):
    def hello(self): print("B")

class C(A):
    def hello(self): print("C")

class D(B, C):
    pass

D().hello()           # B — follows MRO
print(D.__mro__)      # (D, B, C, A, object)

Duck Typing — Python's Approach to Interfaces

Python doesn't have formal interface declarations. Instead, it uses duck typing: if an object has the right methods, it works — no inheritance required.

"If it walks like a duck and quacks like a duck, it's a duck."

# These three classes have NO common parent
class Duck:
    def speak(self) -> str: return "Quack!"
    def move(self)  -> str: return "Waddle"

class Robot:
    def speak(self) -> str: return "Beep boop"
    def move(self)  -> str: return "Roll"

class Human:
    def speak(self) -> str: return "Hello!"
    def move(self)  -> str: return "Walk"

# This function works with ANY object that has speak() and move()
def describe(entity) -> None:
    print(f"Says: {entity.speak()}")
    print(f"Moves: {entity.move()}")

for thing in [Duck(), Robot(), Human()]:
    describe(thing)

Protocols — Structural Typing (Python 3.8+)

Protocol makes duck typing explicit and type-checker-friendly:

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...
    def get_area(self) -> float: ...

class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    def draw(self) -> None:
        print(f"○  Circle r={self.radius}")

    def get_area(self) -> float:
        return 3.14159 * self.radius ** 2

class Square:
    def __init__(self, side: float):
        self.side = side

    def draw(self) -> None:
        print(f"□  Square s={self.side}")

    def get_area(self) -> float:
        return self.side ** 2

# The type hint accepts anything that satisfies Drawable
def render_all(shapes: list[Drawable]) -> None:
    for shape in shapes:
        shape.draw()
        print(f"   Area: {shape.get_area():.2f}")

# Neither Circle nor Square explicitly inherits Drawable
# But they satisfy its structure, so this works perfectly
render_all([Circle(5), Square(4), Circle(2)])
Protocol vs ABC — when to use which

Use ABC when you control the hierarchy and want to enforce implementation via inheritance. Use Protocol when you want to describe a structural contract without forcing inheritance — great for library code that should accept any compatible type.

What's Next?

Python OOP #3 covers special methods in depth — context managers (__enter__/__exit__), iterators (__iter__/__next__), and making your classes work naturally with Python's built-in operators and protocols.

#python#oop#inheritance#abc#protocols

✦ Enjoyed this post?

Get posts like this in your inbox

No spam, just real tutorials when they're ready.

Discussion

Powered by GitHub

Comments use GitHub Discussions — no separate account needed if you have GitHub.