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.
- Write a base class that inherits from
ABCand 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)
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.
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)
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()
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.
- Inherit from
ABCand 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.