runtime
141 / 250
lesson 141 Advanced OOP & Design · advanced

Abstract base classes

Some base classes exist only to be built on: every payment method must say what its fee is, but there is no fee for a payment method in general. An abstract base class states which methods subclasses must supply, and refuses to create objects until they do.

You'll be able to
  • Write a base class that inherits from ABC and marks required methods with @abstractmethod
  • Explain the TypeError raised when an abstract class, or a subclass missing a method, is instantiated
  • Put shared logic in a concrete method that calls the abstract ones (the template method pattern)
The idea

In the Inheritance & Composition module a payment method's subclasses each had a fee method, but nothing *required* it: a new subclass that forgot fee (or misspelt it) was only discovered when some code called it. An abstract base class turns the rule into code. Inherit from ABC (from the standard abc module) and mark each required method with @abstractmethod; its body can be just a docstring.

Python then refuses to create an object of any class that still has abstract methods: the base class itself, and every subclass that hasn't implemented them all. The mistake surfaces when the object is created, with a message naming the missing method.

The base class can also hold concrete methods. A concrete charge that calls the abstract fee is the *template method* pattern: the base class fixes the steps, the subclasses fill in the parts that vary.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Payment methods with a required fee
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    def __init__(self, label):
        self.label = label

    @abstractmethod
    def fee(self, amount):
        """The provider's fee for this amount."""

    def charge(self, amount):
        fee = self.fee(amount)
        return f"{self.label}: charge {amount:.2f} + fee {fee:.2f} = {amount + fee:.2f}"

class Card(PaymentMethod):
    def fee(self, amount):
        return round(amount * 0.015, 2)

class BankTransfer(PaymentMethod):
    def fee(self, amount):
        return 0.2

for method in [Card("Visa 4242"), BankTransfer("IBAN GB33")]:
    print(method.charge(40.0))
try:
    PaymentMethod("generic")
except TypeError as err:
    print(err)
example-2.py · A subclass with the wrong method name
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def fee(self, amount):
        """The provider's fee for this amount."""

class Voucher(PaymentMethod):
    def fees(self, amount):
        return 0

print("classes defined")
voucher = Voucher()
example-3.py · One render method, two formats
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def header(self, columns):
        """The first line(s) of the output."""

    @abstractmethod
    def row(self, values):
        """One record as a line of text."""

    def render(self, records):
        columns = list(records[0])
        lines = [self.header(columns)]
        for record in records:
            lines.append(self.row([str(record[c]) for c in columns]))
        return "\n".join(lines)

class CsvExporter(Exporter):
    def header(self, columns):
        return ",".join(columns)

    def row(self, values):
        return ",".join(values)

class MarkdownExporter(Exporter):
    def header(self, columns):
        return "| " + " | ".join(columns) + " |\n|" + "---|" * len(columns)

    def row(self, values):
        return "| " + " | ".join(values) + " |"

orders = [{"order_id": "O100282", "total": 35.84}, {"order_id": "O100918", "total": 55.72}]
print(CsvExporter().render(orders))
print(MarkdownExporter().render(orders))

When to use it: for a family of classes that must all provide the same methods, especially when the base class has shared logic that depends on them. Mistakes to avoid: forgetting ABC as the base (then @abstractmethod is not enforced); putting real work in an abstract method's body and expecting it to run; and making everything abstract, which leaves nothing to share. When unrelated classes only need to *look* alike, a Protocol (next lesson) is lighter.

Review
  • Inherit from ABC and mark required methods with @abstractmethod.
  • A class with any unimplemented abstract method can't be instantiated: mistakes surface when objects are created.
  • Concrete methods in the base class can call the abstract ones: the template method pattern.
  • Next: checking at run time whether any object fits a Protocol.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Abstract base classes — Advanced OOP & Design · Runtime