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,MappingandIterable. - 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.
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.
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)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
Finish the broken class
In the first example give
Brokenanareamethod that returns 0, then callBroken().describe(). The error disappears because the class is now concrete. - 2
Add a second fake
In the second example write
EchoClientwhosecompletereturns the prompt in capitals, and pass it tosummarise. You added a new client without touchingsummariseor inheriting anything. - 3
Check a property of the contract
Call
isinstance(FakeClient("x"), Closeable). It isFalse. Add aclosemethod toFakeClientand check again.
Code & diagrams
Try creating Shape() yourself. Then implement area in Broken and watch the TypeError disappear.
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 TrueRealClient and FakeClient never inherit from ModelClient. Swap one for the other in summarise() and nothing else changes.
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 FalseIllustrative 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: completeBreak 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.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
class Square(Shape):
pass
Square()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)makesisinstancetrue without inheritance.collections.abcuses__subclasshook__so thatisinstance(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
CloseableorModelClient) 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()orembed()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:
@abstractmethodmust be the innermost decorator (@propertyon top, then@abstractmethod), and the check happens only at instantiation, only for classes whose metaclass isABCMeta(inheritingABCdoes this). A@runtime_checkableprotocol with data members (likename: str) supportsisinstancebutissubclassraisesTypeError, andisinstancechecks are slower than normal ones, so keep them out of hot loops. - ▸
Protocols can also have attributes (
name: str) and@propertymembers. 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
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 aquackmethod 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
An abstract base class comes from the
abcmodule. Inherit fromABCand 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 raisesTypeError: 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
An ABC can also contain ordinary, finished methods that all subclasses share. The
describemethod in the example usesself.area(), which each subclass provides. This is the template method idea: the parent writes the steps and the children fill in the blanks. - 4
The
collections.abcmodule holds ABCs for Python's own protocols:Sequence,Mapping,Iterable,Callable. If you inherit fromSequenceand write just__len__and__getitem__, you getindex,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
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 matchingcompletemethod fits the shape, without inheriting fromModelClient. 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 allowsisinstancechecks of method names only (not signatures). - 6
Protocols are the standard way to build swappable parts. Write your code against
ModelClient. In production pass a real client; in tests pass aFakeClientthat 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
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
What is duck typing, and what is its main weakness?
How does an abstract base class work, and when does Python raise the error for a missing method?
What is a Protocol, and how is it different from an ABC?
Why are protocols useful for testing code that calls an AI model?
Practice
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"].
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.
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
@abstractmethodand explain theTypeErrorfor incomplete subclasses.Done when you can use
collections.abc.Sequenceto get methods for free.Done when you can write a
Protocoland two classes that fit it without inheriting.Done when you can explain how a fake client makes AI code testable.