Frontend Labs
⋯

17. Inheritance, composition, ABCs, and protocols

17.1 Inheritance and super()

class Notifier:
    def send(self, to: str, msg: str) -> None:
        raise NotImplementedError

class EmailNotifier(Notifier):
    def __init__(self, smtp_host: str) -> None:
        self.smtp_host = smtp_host

    def send(self, to: str, msg: str) -> None:
        print(f"[smtp {self.smtp_host}] -> {to}: {msg}")

class AuditedEmailNotifier(EmailNotifier):
    def send(self, to: str, msg: str) -> None:
        print(f"audit: notify {to}")
        super().send(to, msg)          # call the parent's version

AuditedEmailNotifier("mail.local").send("ops@example.com", "disk 91%")
print(AuditedEmailNotifier.__mro__)    # method resolution order

Python supports multiple inheritance, resolved by the MRO (method resolution order, C3 linearization). super() means "next class in the MRO", not "my parent", which is what makes cooperative mixins work. Keep hierarchies shallow; deep inheritance trees are hard to follow in any language.

17.2 Prefer composition

class RetryingNotifier:
    """Wraps any notifier; does not inherit from one."""

    def __init__(self, inner, attempts: int = 3) -> None:
        self.inner = inner
        self.attempts = attempts

    def send(self, to: str, msg: str) -> None:
        for i in range(self.attempts):
            try:
                return self.inner.send(to, msg)
            except ConnectionError:
                if i == self.attempts - 1:
                    raise

RetryingNotifier(EmailNotifier("mail.local")).send("ops@example.com", "hi")

Composition lets you combine behaviours (retrying + audited + rate-limited) without a class for every combination. requests does this with transport adapters mounted on a session (Ch 64).

17.3 ABCs: enforced interfaces

from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def put(self, key: str, data: bytes) -> None: ...
    @abstractmethod
    def get(self, key: str) -> bytes: ...

class MemoryStorage(Storage):
    def __init__(self) -> None:
        self._d: dict[str, bytes] = {}
    def put(self, key: str, data: bytes) -> None:
        self._d[key] = data
    def get(self, key: str) -> bytes:
        return self._d[key]

s = MemoryStorage()
s.put("a", b"1")
print(s.get("a"))
Storage()
# TypeError: Can't instantiate abstract class Storage without an implementation for abstract methods 'get', 'put'

17.4 Protocols: duck typing the type checker understands

ABCs require inheritance. typing.Protocol describes the shape and accepts any class that has it, like a Go interface:

from typing import Protocol

class SupportsSend(Protocol):
    def send(self, to: str, msg: str) -> None: ...

def alert_all(n: SupportsSend, people: list[str]) -> None:
    for p in people:
        n.send(p, "incident started")

class SlackNotifier:                      # does NOT inherit from anything
    def send(self, to: str, msg: str) -> None:
        print(f"[slack] @{to}: {msg}")

alert_all(SlackNotifier(), ["ada", "linus"])   # the type checker accepts this

Rule of thumb: use a Protocol for "anything with these methods" in function signatures; use an ABC when you want to share implementation and force subclasses to fill in specific methods. The Go guide's interfaces and Rust's traits are the same idea with compiler enforcement.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36