Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~27 min· topic 10 of 12

Topic 8.10

Abstract Base Classes and Protocols

In one line

An abstract base class (ABC) is a parent that cannot be instantiated and forces subclasses to implement certain methods. A Protocol describes a required shape (method names and signatures) so any class that fits can be used, without inheriting, which is how duck typing becomes checkable.

Think of it like this

A driving licence test and a power plug. The test says: "You must be able to steer, brake and signal". It does not care what car you drive. A contract names what a thing must be able to do. An abstract base class is a contract that you sign by inheriting from it (inheritance: Topic 8.5). A protocol is a contract that is satisfied simply by having the right abilities, like a plug that fits the socket without signing anything.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Duck typing
Using an object based on the methods it has, not its class. "If it quacks like a duck…".
Abstract base class (ABC)
A class meant to be inherited from, that cannot be instantiated if abstract methods are missing.
`@abstractmethod`
Marks a method that every concrete subclass must implement.
Concrete class
A class that implements all abstract methods and can be instantiated.
Protocol
A description of required methods and signatures. A class satisfies it by having them, without inheriting.
Structural typing
Types match when their structure (methods) fits, not because of inheritance.
Nominal typing
Types match only if one inherits from the other by name.
`collections.abc`
Module of ABCs for built-in behaviours such as Sequence, Mapping and Iterable.
Fake (test double)
A simple stand-in object used in tests instead of a real, slow or paid service.

Step by step

01Duck typing and its weak spot

make_noise(thing) just calls thing.quack(). A Quacker and a Robot both work. A Rock fails with AttributeError: 'Rock' object has no attribute 'quack', but only at the moment the call runs, possibly deep inside a program.

The contract exists only in the programmer's head. Both ABCs and protocols write it down.

02An ABC with an abstract method

Shape(ABC) declares area as abstract. The body is just a docstring, because subclasses must supply the real code. describe is a normal method that uses area.

main.pywhole filepython
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        """Return the area."""

    def describe(self):
        return type(self).__name__ + " with area " + str(self.area())

03Incomplete subclasses fail early

Shape() raises a TypeError, and so does Broken(), which forgot to implement area. The error appears at creation time and names the missing method, so a half-finished class never gets as far as running.

Square implements area, so Square(4).describe() works and isinstance(Square(1), Shape) is True.

04Free methods from collections.abc

Countdown(Sequence) writes only __len__ and __getitem__. Sequence supplies index, count, in, iteration and reversed. Built-in types also count: isinstance([1], Sequence) is True because lists are registered with it.

05A Protocol describes a shape

ModelClient is a Protocol with one method, complete. RealClient and FakeClient do not mention ModelClient anywhere, but both have a matching complete, so both fit. The type hints (prompt: str, -> str) are read by tools like mypy or your editor (Phase 10), not by Python at run time.

Python ignores type hints when running. The Protocol's value is that a type checker can prove, before you run anything, that the object you pass has the required methods.

main.pywhole filepython
from typing import Protocol


class ModelClient(Protocol):
    def complete(self, prompt: str) -> str: ...


def summarise(client: ModelClient, text: str) -> str:
    return client.complete("Summarise: " + text)
A Protocol describes a shapediagram
Rendering diagram…

06A fake client for tests

FakeClient returns a canned answer and records every prompt in calls. A test can assert on fake.calls without any network or cost. This is the pattern Phase 16 uses to test code that calls a model.

07runtime_checkable and its limit

Adding @runtime_checkable lets isinstance(File(), Closeable) work. It only checks that a method named close exists, not its parameters or return type. Use it for simple checks, and rely on a type checker for the full contract.

Try it yourself

  1. 1

    Finish the broken class

    In the first example give Broken an area method that returns 0, then call Broken().describe(). The error disappears because the class is now concrete.

  2. 2

    Add a second fake

    In the second example write EchoClient whose complete returns the prompt in capitals, and pass it to summarise. You added a new client without touching summarise or inheriting anything.

  3. 3

    Check a property of the contract

    Call isinstance(FakeClient("x"), Closeable). It is False. Add a close method to FakeClient and check again.

Code & diagrams

Abstract base classes and collections.abc New tab

Try creating Shape() yourself. Then implement area in Broken and watch the TypeError disappear.

Sign in to run this example in your browser.

Expected output

TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
TypeError: Can't instantiate abstract class Broken without an implementation for abstract method 'area'
Square with area 16
True True
[4, 3, 2, 1] 4 True 2 1 [1, 2, 3, 4]
True True True
Duck typing, a Protocol and a fake model client New tab

RealClient and FakeClient never inherit from ModelClient. Swap one for the other in summarise() and nothing else changes.

Sign in to run this example in your browser.

Expected output

Quack! beep-quack
AttributeError: 'Rock' object has no attribute 'quack'
Short version.
['Summarise: a long article']
(a real model would answer: Summarise: x)
True False
What a type checker says about a wrong fit (not runnable)bash

Illustrative output of a type checker; the wording varies by tool and version. Python itself would only fail when the missing method is called.

$ mypy app.py
app.py:31: error: Argument 1 to "summarise" has incompatible type "Rock"; expected "ModelClient"  [arg-type]
app.py:31: note: "Rock" is missing following "ModelClient" protocol member:
app.py:31: note:     complete
Contract by inheriting versus by shapediagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Instantiate a class that skipped an abstract method

Create a subclass that does not implement the abstract area.

main.pywhole filepython
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        ...


class Square(Shape):
    pass


Square()
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 14, in <module>
Square()
~~~~~~^^
TypeError: Can't instantiate abstract class Square without an implementation for abstract method 'area'

Myth vs fact

Myth

Python checks that an object matches a Protocol when you call a function.

Fact

Not at all. Python ignores type hints at run time. A type checker (or your editor) does the check before you run. Only @runtime_checkable allows a limited isinstance test for method names.

Myth

You must use ABCs to write correct Python.

Fact

Most Python code just uses duck typing. ABCs and protocols are for places where a clear contract helps: plug-in systems, libraries and swappable components.

Myth

A class must inherit from a Protocol to satisfy it.

Fact

That is the point of a Protocol: matching methods are enough. (A class may inherit from one explicitly, for example to get default methods, but need not.)

Myth

An abstract method cannot have any code.

Fact

It can, and subclasses can call it with super(). It only means subclasses must override it before the class can be instantiated.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    ABCs also support virtual subclasses: Shape.register(SomeClass) makes isinstance true without inheritance. collections.abc uses __subclasshook__ so that isinstance(x, Iterable) checks for __iter__ automatically.

  • ▸

    Prefer protocols for function parameters (accept anything that fits) and ABCs or concrete classes for return types you control. Narrow protocols with one or two methods (like Closeable or ModelClient) are easier to satisfy and to fake than broad ones.

  • ▸

    Dependency injection with protocols is how you keep AI code testable and swappable between providers: define complete() or embed() in your own terms, adapt each vendor's SDK behind it, and test the rest with fakes. Adapters keep a vendor change from touching your whole codebase.

  • ▸

    Details that catch people out: @abstractmethod must be the innermost decorator (@property on top, then @abstractmethod), and the check happens only at instantiation, only for classes whose metaclass is ABCMeta (inheriting ABC does this). A @runtime_checkable protocol with data members (like name: str) supports isinstance but issubclass raises TypeError, and isinstance checks are slower than normal ones, so keep them out of hot loops.

  • ▸

    Protocols can also have attributes (name: str) and @property members. A type checker matches them structurally, including whether an attribute is read-only. Methods that must be implemented are written with ... as the body.

Remember this

  1. 1

    Python is built on duck typing: "if it walks like a duck and quacks like a duck, it is a duck". A function that calls thing.quack() does not check the type; any object with a quack method works. This is flexible, but a missing method is found only when the code runs (AttributeError). ABCs and protocols add a way to state and check the contract.

  2. 2

    An abstract base class comes from the abc module. Inherit from ABC and mark required methods with @abstractmethod. You cannot create an object from the ABC, and you cannot create one from a subclass that has not implemented every abstract method: Python raises TypeError: Can't instantiate abstract class ... without an implementation for abstract method .... The error appears when the object is created, not later when the method is first called.

  3. 3

    An ABC can also contain ordinary, finished methods that all subclasses share. The describe method in the example uses self.area(), which each subclass provides. This is the template method idea: the parent writes the steps and the children fill in the blanks.

  4. 4

    The collections.abc module holds ABCs for Python's own protocols: Sequence, Mapping, Iterable, Callable. If you inherit from Sequence and write just __len__ and __getitem__, you get index, count, __contains__, __iter__ and __reversed__ for free. isinstance(x, Iterable) also works for any class that defines __iter__, with no inheritance: the ABC checks the methods.

  5. 5

    A Protocol (from typing) describes a shape: class ModelClient(Protocol): def complete(self, prompt: str) -> str: ... (the ... means "body left empty on purpose"). Any class with a matching complete method fits the shape, without inheriting from ModelClient. This is structural typing: types match by structure, not by family tree. A static type checker (Phase 10) verifies it before the program runs. Python itself does not check at runtime unless you add @runtime_checkable, which allows isinstance checks of method names only (not signatures).

  6. 6

    Protocols are the standard way to build swappable parts. Write your code against ModelClient. In production pass a real client; in tests pass a FakeClient that returns canned text and records the prompts. The two classes share nothing but method names. Phase 16 builds on exactly this when testing code that calls AI models.

  7. 7

    Which to choose? Use a Protocol when you describe what a function needs from its arguments (especially for classes you do not own). Use an ABC when you own the family and want shared code plus a hard runtime error for incomplete subclasses. Often the simplest answer is neither: a plain function argument, as Topic 8.11 shows.

Explain it without notes

01

What is duck typing, and what is its main weakness?

02

How does an abstract base class work, and when does Python raise the error for a missing method?

03

What is a Protocol, and how is it different from an ABC?

04

Why are protocols useful for testing code that calls an AI model?

Practice

01

Write an ABC Notifier with an abstract method send(message) and a concrete method notify_all(messages) that calls send for each. Make PrintNotifier that prints each message. Run it on ["a", "b"].

02

Write a Protocol Speaker with speak() -> str, two unrelated classes Cat and Robot with speak, and a function chorus(speakers) that returns the list of what each says.

03

Write a FakeClock with a method now() returning a number you set, and a function age(clock, born) that returns clock.now() - born. Show that the test is deterministic.

Trade-offs

  • ↔

    ABCs give an immediate, runtime error for incomplete subclasses, at the cost of forcing inheritance. They suit families you control. For classes you do not control, a Protocol fits without changes.

  • ↔

    Protocols keep code flexible and testable, but are only checked by external tools. Without a type checker in your workflow, a Protocol is documentation. With one, it is a proof.

Done when you can

  • Done when you can explain duck typing and its weak spot.

  • Done when you can write an ABC with @abstractmethod and explain the TypeError for incomplete subclasses.

  • Done when you can use collections.abc.Sequence to get methods for free.

  • Done when you can write a Protocol and two classes that fit it without inheriting.

  • Done when you can explain how a fake client makes AI code testable.