Advanced OOP

Python Advanced
Course 3 ยท Chapter 1 ยท Advanced OOP

๐Ÿ—๏ธ Advanced OOP

Welcome to Python Advanced. Course 2 covered classes, inheritance, and dunder methods. This chapter pushes further into territory unique to Python's object system: abstract base classes that enforce a contract on subclasses, multiple inheritance and the Method Resolution Order that makes it deterministic, and a short, honest introduction to metaclasses โ€” one of Python's more advanced (and rarely-needed) features.

๐Ÿ“œ Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass              # no implementation โ€” subclasses MUST provide one

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

    def area(self):
        return self.side ** 2

Shape()      # TypeError: Can't instantiate abstract class Shape with abstract method area
Square(4).area()   # 16 โ€” fine, Square actually implements area()

An abstract base class (inheriting from ABC) can declare methods with @abstractmethod that have no real implementation โ€” just a contract. Python enforces this at instantiation time: you literally cannot create a Shape() instance, and any subclass that fails to override area() can't be instantiated either. This is stronger than Course 2, Chapter 2's plain Shape base class (which just returned 0 and quietly allowed instantiation) โ€” here, forgetting to implement area() is caught immediately, not silently wrong.

๐Ÿงฌ Multiple Inheritance

class Flyer:
    def move(self):
        return "flying"

class Swimmer:
    def move(self):
        return "swimming"

class Duck(Flyer, Swimmer):   # inherits from BOTH
    pass

Duck().move()   # "flying" โ€” Flyer is listed first, so it wins

Unlike Course 2's single-parent inheritance, Python allows a class to inherit from multiple parents at once. When more than one parent defines the same method, Python needs a deterministic rule to decide which one actually runs โ€” that rule is the MRO.

๐Ÿ”— The Method Resolution Order (MRO)

print(Duck.__mro__)
# (<class 'Duck'>, <class 'Flyer'>, <class 'Swimmer'>, <class 'object'>)

The Method Resolution Order is the exact sequence Python searches through to find a method โ€” itself, then each parent, left to right, following Python's C3 linearization algorithm. Duck.move() resolves to Flyer.move specifically because Flyer appears before Swimmer in class Duck(Flyer, Swimmer):. Calling Duck.__mro__ (or Duck.mro()) shows the exact order any method lookup will follow.

โš  Multiple Inheritance Gets Confusing Fast โ€” Prefer Composition

The classic "diamond problem" โ€” two parent classes that both inherit from a common grandparent, each overriding the same method differently โ€” is exactly what the MRO exists to resolve deterministically. But deterministic doesn't mean easy to read: as inheritance chains get deeper, predicting which method actually runs from the class definition alone gets genuinely hard. In real code, favor composition (an object holding a reference to another object, and delegating to it) over multiple inheritance wherever it achieves the same goal โ€” it's usually far easier to reason about.

๐Ÿงช A Metaclasses Intro

Just as a class is a blueprint for creating objects, a metaclass is a blueprint for creating classes โ€” every class in Python is itself an instance of a metaclass, and by default, that metaclass is type:

print(type(42))              # <class 'int'> โ€” 42 is an instance of int
print(type(Duck))            # <class 'type'> โ€” Duck is an instance of type

class LoudMeta(type):                   # a custom metaclass, inheriting from type
    def __new__(mcs, name, bases, namespace):
        print(f"Creating class: {name}")
        return super().__new__(mcs, name, bases, namespace)

class Widget(metaclass=LoudMeta):   # Widget's CREATION triggers LoudMeta.__new__
    pass
# Creating class: Widget โ€” printed the moment the class body finishes, not when Widget() is called

A metaclass hooks into the process of defining a class, not creating instances of it โ€” this runs once, when Python processes the class Widget: statement itself. Metaclasses are genuinely rare to write in application code; they show up mainly inside frameworks (Django's ORM uses one to turn model class attributes into database fields automatically). Recognizing what a metaclass is matters more than needing to write your own โ€” most Python code never does.

Inheritance Models: Go vs Kotlin vs Python

LanguageApproach
GoNo inheritance at all โ€” interfaces (implicit satisfaction) plus struct embedding cover similar ground, with no diamond problem possible since there's no class hierarchy to create one.
KotlinSingle inheritance only (one open class parent), but a class can implement multiple interfaces โ€” Kotlin sidesteps the diamond problem by only allowing ambiguity between interface default methods, which it forces you to resolve explicitly with super<InterfaceName>.
PythonTrue multiple inheritance from multiple concrete classes, resolved deterministically via the MRO โ€” more powerful, and correspondingly easier to misuse.

Kotlin's "single class, multiple interfaces" model is a deliberate middle ground โ€” it gets most of multiple inheritance's benefit while designing away the diamond problem's worst confusion, something Python's fuller model doesn't do for you automatically.

ABC / @abstractmethod

Enforces a contract โ€” a class can't be instantiated until it implements every abstract method.

Multiple inheritance

class C(A, B): โ€” inherits from more than one parent at once.

MRO

The deterministic order Python searches parents in โ€” view it with Class.__mro__.

Metaclass

A "class of a class" โ€” type by default; hooks into class creation itself.

๐Ÿ’ป Coding Challenges

Challenge 1: Enforced Payment Interface

Write an abstract base class PaymentMethod(ABC) with an abstract method process(amount). Write a subclass CreditCard that implements it (just returning a confirmation string is fine). Prove that instantiating PaymentMethod() directly raises a TypeError.

Goal: Practice defining and enforcing an abstract base class contract.

โ†’ Solution

Challenge 2: Trace the MRO

Given three classes A, B(A), and C(A), and a fourth class D(B, C), print D.__mro__ and, in a comment, explain in one sentence why A appears only once despite being an ancestor of both B and C.

Goal: Get hands-on with reading an actual MRO output for a real diamond-shaped hierarchy.

โ†’ Solution

Challenge 3: Composition Over Multiple Inheritance

Rewrite this chapter's Duck(Flyer, Swimmer) example using composition instead โ€” a Duck class that holds a Flyer instance and a Swimmer instance as attributes, with its own fly() and swim() methods that delegate to them.

Goal: Practice the composition alternative this chapter's warn-box recommends, and see how it avoids MRO ambiguity entirely.

โ†’ Solution

๐ŸŽฏ What's Next

Next chapter: Iterators & Context Managers Deep Dive โ€” building a custom iterator, contextlib, and __enter__/__exit__.