---
title: 'Abstract Base Classes and Protocols'
source: 'https://academia.sh/en/courses/python-object-and-types/abstract-base-classes-and-protocols'
course: 'Object-Oriented Python and Types'
language: en
updated: '2026-08-17T18:10:29+00:00'
license: 'CC BY-SA 4.0'
---

# Abstract Base Classes and Protocols

1 of three candidates passes the ancestry test, 3 pass the method test, and 3 answer the call; in the two tests' blind spots, 3 of three candidates are accepted but only 1 answers the call.

The previous lesson counted what a class writes by looking at its dict; the ones before it
searched for the answer in the class tree. Both methods rest on the same assumption: knowing
what an object can do requires knowing **which class** it comes from. But the fourth lesson
showed the opposite — joining came not from the type's name, it came from the methods
written. The second lesson's `Wrapper` class is the concrete form of this duality: it derives
from nothing, and answers all four of the four capabilities.

This lesson turns the duality into a test. Whether a contract is satisfied can be asked in
two separate ways. A **nominal** test searches for ancestry: "did this object derive from
that contract?" A **structural** test searches for methods: "does this object carry these
names?" Which of the two does the same object pass, and does passing the test say it will
actually answer the call? For the question to be measurable, three separate things are asked
of every candidate: the two tests' results, and **the call itself**. Where the three do not
agree is this lesson's finding.

## Two Tests, One Object

Two contracts are set up, and both ask for the same two capabilities: `source` and `format`.
The first is an **abstract base class**; a class satisfying it must derive from it. The
second is a **protocol**; the test only looks at whether the names exist.

Three candidates are tested. `Document` comes from an inheritance tree and inherits both
capabilities from three separate classes. `Wrapper` derives from nothing and writes both
capabilities in its own body. `Declarer` derives from the abstract base and writes both
bodies too.

- **CD72** — The rig's four-class tree and `Wrapper` are the shared definition's form; the
  contracts are set up separately and the tree is untouched.
- **CD73** — Both contracts ask for the same two names; the only difference between them is
  what the test looks at.
- **CD74** — Three things are measured separately for every candidate: whether the names are
  actually found, the two tests' results, and the result the call gives.
- **CD75** — The last column is the string `format()` returns; it is the same result the
  second lesson measured, printed again here.

```python
"""Nominal and structural contract: which test does the same object pass."""

from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable


class Record:
    def source(self):
        return "Record"

    def format(self):
        return f"<{self.source()}>"


class Timed(Record):
    def stamp(self):
        return "time"


class Signed(Record):
    def source(self):
        return "Signed"

    def signature(self):
        return "signature"


class Document(Timed, Signed):
    pass


class Wrapper:
    def __init__(self):
        self.timed = Timed()
        self.signed = Signed()

    def source(self):
        return self.signed.source()

    def stamp(self):
        return self.timed.stamp()

    def signature(self):
        return self.signed.signature()

    def format(self):
        return f"<{self.source()}>"


class Formattable(ABC):
    """Nominal contract: demands ancestry."""

    @abstractmethod
    def source(self):
        ...

    @abstractmethod
    def format(self):
        ...


@runtime_checkable
class FormattableProtocol(Protocol):
    """Structural contract: looks for method names."""

    def source(self): ...

    def format(self): ...


class Declarer(Formattable):
    """Declares the contract by ancestry and writes both bodies."""

    def source(self):
        return "Declarer"

    def format(self):
        return f"<{self.source()}>"


CANDIDATES = (("Document", Document()), ("Wrapper", Wrapper()), ("Declarer", Declarer()))

print(f"{'candidate':<10s} {'has both methods':>17s} {'ancestry test':>15s} "
      f"{'method test':>13s} {'format() result':>16s}")
for name, n in CANDIDATES:
    print(f"{name:<10s} "
          f"{str(all(hasattr(n, y) for y in ('source', 'format'))):>17s} "
          f"{str(isinstance(n, Formattable)):>15s} "
          f"{str(isinstance(n, FormattableProtocol)):>13s} {n.format():>16s}")

nominal = sum(1 for _, n in CANDIDATES if isinstance(n, Formattable))
structural = sum(1 for _, n in CANDIDATES if isinstance(n, FormattableProtocol))
print(f"\ncandidates {len(CANDIDATES)}, passing ancestry test {nominal}, "
      f"passing method test {structural}, answering the call {len(CANDIDATES)}")
```

```
candidate   has both methods   ancestry test   method test  format() result
Document                True           False          True         <Signed>
Wrapper                 True           False          True         <Signed>
Declarer                True            True          True       <Declarer>

candidates 3, passing ancestry test 1, passing method test 3, answering the call 3
```

All three of the three candidates answer the call. Passing the ancestry test is **1**,
passing the method test is **3**. The two objects in between — `Document` and `Wrapper` — do
everything the contract asks for and still fail the nominal test, because that test looks not
at the work done, but at the **declared ancestry**.

The two candidates fail for separate reasons, and this shows how narrow the test is.
`Wrapper` does not even know the contract exists. `Document` carries both bodies the
contract asks for through inheritance — as the second lesson measured, `format`'s body is in
`Record`, `source`'s in `Signed`. Both sit exactly where they should, both work, and the test
still gives `False`.

The structural test accepts both objects, because it asks a different question: are the
names the contract asks for present on this object? If they are, where they come from does
not matter — its own body, inheritance, or composition. The contract here is defined as a
**surface**, not an **ancestry**.

The two tests' cost is not the same either. The nominal test looks at a single ancestry
relationship; its result is known the moment the class is built and is not recomputed on
every test. The structural test has to search the object for every name the contract asks
for, and as the contract grows, the number of names searched grows with it. The contract in
this measurement asks for two names; a contract asking for ten names makes the same test do
ten searches. The split does not change direction here either: the nominal test is cheap and
narrow, the structural test is expensive and broad.

Choosing between an interface and an abstract class, narrowing interfaces to the client, and
preserving a contract across subtypes were established in the Software Design and
Architecture Principles curriculum; **those discussions are not repeated here.** The only
thing measured here is **which test the same object passes.**

The measurement does not force a choice either; it says under which condition each test is
right. If we write both sides of the contract ourselves, a nominal declaration pays off: the
declaring class states clearly what it must provide, and is stopped if it falls short. If
**others** will write the objects satisfying the contract, a nominal declaration becomes a
burden — demanding that every acceptable object derive from our definition leaves out objects
like `Document` and `Wrapper` that do the job completely. This is why most of the
standard library's container contracts carry a hook that searches for bodies: when a class
writes the required methods, it counts as a provider of the contract without setting up any
ancestry at all.

## When the Contract Is Enforced

The nominal contract also has an enforcement side. A class deriving from an abstract base has
to write all the declared bodies. But at which point is this requirement checked — when the
class is defined, or when an instance is built?

- **CD76** — Both classes derive from the same abstract base; one writes both bodies, the
  other only one.
- **CD77** — The "at definition time" column is read from the block running: since both
  classes can be defined, its value is constant. The "building an instance" column is the
  call's result, and a falling call records the **exception's name** with an exclamation
  prefix.

```python
class Incomplete(Formattable):
    """Declares the contract but writes only one of the two bodies."""

    def source(self):
        return "Incomplete"


def attempt(action):
    try:
        action()
    except Exception as e:
        return f"!{type(e).__name__}"
    return "passed"


print(f"{'class':<10s} {'at definition time':>19s} {'building an instance':>21s} "
      f"{'unwritten method':>17s}")
for cls in (Declarer, Incomplete):
    print(f"{cls.__name__:<10s} {'built':>19s} {attempt(cls):>21s} "
          f"{str(sorted(cls.__abstractmethods__)):>17s}")
```

```
class       at definition time  building an instance  unwritten method
Declarer                 built                passed                []
Incomplete               built            !TypeError        ['format']
```

The incomplete class **can be defined**. The error is not raised at definition time, it is
raised when an attempt is made to build the first instance, and the last column gives the
missing name. The distinction matters in practice: the incomplete class can sit in a file, be
imported, and serve as a base for other classes. Nothing happens as long as it is never
instantiated itself — this is exactly why the abstract base itself cannot be instantiated
either. Placing the check at this point is the right choice: an incomplete class can still be
a useful definition, but an incomplete **object** cannot answer the call while claiming to
carry the contract. What is stopped is not the definition, it is the **claim**.

This is notable as the one contract that is actually enforced. The third lesson measured that
naming blocks **0** access; here, the declared contract is genuinely enforced, and an
incomplete implementation is stopped at runtime. But the enforcement point is narrow: only
the **presence of the body** is tested. Whether the written body does the right thing, or
returns the right value, is not part of this test at all.

Tying the check to instance construction also grants a convenience. The set of unwritten
methods is recomputed for every subclass; a class can stay as an intermediate layer and
deliberately leave some bodies unwritten, and a class at the end of the chain completes the
gaps. As long as the intermediate class is never instantiated, it meets no obstacle. The
contract can thus be split across more than one class — what is checked is not a single
class's own body, it is the **entirety** of the bodies gathered along the resolution order.

## The Two Tests' Blind Spots

Objects can be built that both tests accept but that do not answer the call. The two blind
spots mirror each other: the nominal test reads ancestry and does not look at the body; the
structural test reads the name and does not look at the signature.

- **CD78** — The first candidate writes no body at all and is **registered by hand** with the
  contract; the registration changes only the ancestry information.
- **CD79** — The second candidate carries both names, but both methods demand **one extra
  argument**; the names are correct, the calling form is wrong.
- **CD80** — The third candidate is the previous measurement's correct class, added as a
  comparison baseline. The call column is made with no arguments on every candidate.

```python
class Empty:
    """Writes no body at all; is registered with the contract by hand."""


Formattable.register(Empty)


class WrongSignature:
    """Carries both names, but both demand one extra argument."""

    def source(self, extra):
        return f"WrongSignature-{extra}"

    def format(self, extra):
        return f"<{self.source(extra)}>"


BLIND_SPOTS = (("Empty", Empty()), ("WrongSignature", WrongSignature()),
               ("Declarer", Declarer()))

print(f"{'candidate':<14s} {'ancestry test':>15s} {'method test':>13s} "
      f"{'format() call':>15s}")
for name, n in BLIND_SPOTS:
    print(f"{name:<14s} {str(isinstance(n, Formattable)):>15s} "
          f"{str(isinstance(n, FormattableProtocol)):>13s} "
          f"{attempt(n.format) if hasattr(n, 'format') else '!AttributeError':>15s}")

accepted = sum(1 for _, n in BLIND_SPOTS
               if isinstance(n, Formattable) or isinstance(n, FormattableProtocol))
answering = sum(1 for _, n in BLIND_SPOTS
                if hasattr(n, "format") and attempt(n.format) == "passed")
print(f"\ncandidates {len(BLIND_SPOTS)}, passing at least one test {accepted}, "
      f"answering the call {answering}")
```

```
candidate        ancestry test   method test   format() call
Empty                     True         False !AttributeError
WrongSignature           False          True      !TypeError
Declarer                  True          True          passed

candidates 3, passing at least one test 3, answering the call 1
```

**3** of three candidates pass at least one test; answering the call is **1**. The first row
is the nominal test's blindness: `Empty` writes no body at all, is registered with the
contract by hand, and the test gives `True`. Registration changes only ancestry information;
it never asks about the bodies written. The call falls with `AttributeError` — an object that
passed the test does not even carry the name being searched for.

The second row is the structural test's blindness. `WrongSignature` carries both names, and
the test gives `True`; but both methods demand one extra argument, and a no-argument call
falls with `TypeError`. The structural test looks at **whether the name exists**; it does not
look at its parameters, its return value, or what its body does. The surface's name matches,
its shape does not.

The two rows together give this lesson's finding. The nominal test verifies a **declared
intent**; it does not verify that the intent was fulfilled. The structural test verifies
**existing names**; it does not verify that those names are callable. Neither says "this
object answers the call," and an object accepted by both can still be wrong.

This finding's measured counterpart is completed when set next to the first table. In the
first measurement, **3** of three candidates answered the call, and the nominal test accepted
only **1** of them — the test was too **narrow**. In the third measurement, both tests each
accepted one object that did not answer the call — the tests were too **wide**. The same
tool under-filters in one direction and over-filters in the other. The right contract is not
in the test itself, it is in choosing the test **knowing what it measures**.

Where the blind spots close is also known, and it differs for each. The nominal test's blind
spot is specific to hand-written registration; when ancestry is set up through inheritance,
the second measurement's check kicks in, and an instance of an incompletely implemented class
can never even be built. Hand registration is a declaration that bypasses this check and
leaves the responsibility with whoever registers it. The structural test's blind spot does
not close: a name search done at runtime can never see the signature under any condition.
The side that could see the signature is a side that reads the code **without running it.**

This split turns into a writing rule in practice. A test passing is not a guarantee the call
will be answered; the test is only a **pre-filter**. Testing an incoming object is not a
substitute for calling it — and since an object that has passed the test can still fall on
the call, who closes the gap between the test and the call has to be decided separately.

## Summary

- The nominal test searches for ancestry, the structural test searches for method names; of
  the same three candidates, **1** passes the first, **3** pass the second, and **3** answer
  the call.
- `Document` and `Wrapper` both carry the two bodies the contract asks for — one through
  inheritance, the other through composition — and both fail the nominal test; the test looks
  not at the work done, but at the declared ancestry.
- The contract declared by an abstract base is enforced **not at definition time**, but when
  the first instance is built: an incomplete class can be defined, its instantiation falls
  with `TypeError`, and the unwritten body's name is reported.
- The nominal test's blind spot is the body: a class writing no body at all passes the test
  once registered by hand with the contract, and the call falls with `AttributeError`.
- The structural test's blind spot is the signature: a class carrying both names but
  demanding an extra argument passes the test, and the call falls with `TypeError`.
- **3** of three blind-spot candidates pass at least one test, and **1** answers the call:
  passing the test does not say the call will be answered.

## Next Step

Throughout this topic the contract was always declared **in code**. The abstract base
demanded bodies be written and did not let an instance be built when they were not; the
protocol demanded names exist and gave the test's result at runtime. Both stayed incomplete,
but both were **working** checks: what was declared was compared against what existed, and
the result came back as a value.

The last measurement's gap calls for a different kind of declaration. `WrongSignature` passed
the test, because all the test saw was names; **how many arguments** the method takes, what
the arguments look like, and what it returns entered no test at all. This information can be
written into code — Python gives a spelling for it, and the previous lesson's short
definition already used that same spelling as its field declaration. The next topic starts
exactly here: once a contract is put down **in writing**, who tests it, what does runtime do
with that writing, and where does the error surface when what is written and what is done
come apart?
