Skip to main content
CodeOath
← All posts

Python105 min total · 18 parts

Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas

Part 15 of 18 · ~3 min

Inheritance, super(), and MRO

class Report:
    def __init__(self, entries):
        self.entries = entries
    def summary(self):
        return f"{len(self.entries)} entries"

class ErrorReport(Report):
    def summary(self):
        base = super().summary()               # calls Report's summary() explicitly
        errors = [e for e in self.entries if e.status >= 500]
        return f"{base}, {len(errors)} server errors"

ErrorReport(morning_entries).summary()   # "1842 entries, 12 server errors"

ErrorReport.summary isn't replacing Report.summary, it's extending it — running the parent's version first and building on top of whatever comes back, rather than duplicating the f"{len(self.entries)} entries" logic a second time. super().summary() reaches that parent implementation without ErrorReport ever having to name Report directly, which looks like a minor convenience right up until a class sits in the middle of a longer chain, where hardcoding a specific ancestor's name instead of asking super() for "whatever comes next" quietly breaks the moment that chain changes shape.

Multiple inheritance and the MRO

The watcher's reports grow two independent specializations — one about server errors, one about suspicious IPs — and eventually a report that needs to be both at once. ErrorReport's summary gets trimmed down slightly for what's coming, so the chain of super() calls stays readable across three levels instead of two:

class ErrorReport(Report):
    def summary(self):
        return "errors -> " + super().summary()

class SecurityReport(Report):
    def summary(self):
        return "security -> " + super().summary()

class IncidentReport(ErrorReport, SecurityReport):
    def summary(self):
        return "incident -> " + super().summary()

IncidentReport(morning_entries).summary()   # "incident -> errors -> security -> 1842 entries"
IncidentReport.__mro__                       # (IncidentReport, ErrorReport, SecurityReport, Report, object)

IncidentReport has two parents, and each of those has its own parent, Report, in common — a diamond, not a tree. Python resolves the ambiguity that shape creates by computing a single linear ordering of the whole hierarchy for every class, called the MRO, using an algorithm named C3 linearization; you can print it directly as IncidentReport.__mro__ and see exactly the sequence shown above. super() was never asking for "the class one level up" — it's asking for "whoever comes next in this ordering," which is why ErrorReport's super().summary() call lands on SecurityReport, not on Report directly, even though ErrorReport was only ever written to know about Report. It has no idea SecurityReport exists, and it doesn't need to; the MRO is computed for the combined class, IncidentReport, and every super() call in the chain defers to it. Coming from a language without multiple inheritance, expect this to take a few tries to feel natural.

isinstance(daily_report, Report) already works correctly for every one of these classes for free, because Python checks the whole MRO, not just the exact type — isinstance(IncidentReport(...), Report) is True despite Report being three steps up the chain.

Report itself stayed concrete and instantiable on purpose throughout this reference, so plain batches never needed a subclass. But the pattern of forcing every subclass to implement a method — worth knowing even though this particular Report doesn't use it — is abc.ABC:

from abc import ABC, abstractmethod

class Exportable(ABC):
    @abstractmethod
    def to_json(self):
        ...   # every subclass MUST provide this — Exportable itself can no longer be instantiated directly

Exportable()   # TypeError: Can't instantiate abstract class Exportable without an implementation for abstract method 'to_json'

class ExportableErrorReport(ErrorReport, Exportable):
    def to_json(self):
        return {"errors": [e.status for e in self.entries if e.status >= 500]}

That's a real, enforced constraint, checked the moment you try to create an instance — not a comment saying "subclasses should implement this" that nobody reads until something breaks at runtime with an AttributeError three layers deep. Had Report needed to guarantee every subclass could export itself, inheriting from Exportable alongside Report would have made forgetting to_json impossible to ship rather than merely undesirable.