Abstract base classes and protocols
Define what an interface looks like in Python, using duck typing, abstract base classes that refuse incomplete subclasses, and Protocols that checkers verify without inheritance.
What you will learn
- Explain duck typing and its trade-off
- Define an abstract base class with @abstractmethod, and explain when instantiation fails
- Write a typing.Protocol and use it in a type hint without inheriting from it
- Choose between an ABC and a Protocol for a given design
- Combine @abstractmethod with @override
On this page
When several classes should be usable in the same place, the code that uses them needs to know what they have in common. That shared promise is an interface: a set of methods and what they mean. Python offers three ways to express one, from the loosest to the strictest.
Duck typing
If it walks like a duck and quacks like a duck, treat it as a duck. Python does not ask for a family tree. A function simply calls the method it needs:
class Duck: def speak(self): return "Quack"
class Robot: def speak(self): return "Beep"
def announce(thing): print(thing.speak())
announce(Duck())announce(Robot())QuackBeepDuck and Robot share no parent. announce works with both because both have a speak method. This is duck typing, and it is how most Python code is written. It needs no ceremony, and it is the reason the language feels flexible.
The cost is that mistakes surface late, when the call is made:
class Stone: pass
def announce(thing): print(thing.speak())
try: announce(Stone())except AttributeError as error: print("AttributeError:", error)AttributeError: 'Stone' object has no attribute 'speak'Nothing told the author of Stone that speak was expected. The next two tools make the expectation explicit, one at run time and one for type checkers.
Abstract base classes
An abstract base class (ABC) is a parent class that declares methods its subclasses must provide. Inherit from ABC and mark each required method with @abstractmethod:
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): """Return the area as a number."""
class Square(Shape): def __init__(self, side): self.side = side
def area(self): return self.side**2
try: Shape()except TypeError: print("TypeError: cannot create an abstract Shape")print(Square(3).area())print(isinstance(Square(3), Shape))TypeError: cannot create an abstract Shape9TrueShape cannot be instantiated, because it is incomplete. Square supplies area, so it is concrete and works normally. Because Square inherits from Shape, isinstance(Square(3), Shape) is True. The body of the abstract method is only a docstring, which says what implementers should do.
The wording of the TypeError message varies between Python versions, so the examples catch the exception type and print their own text.
The check happens when you create an instance. A subclass that forgets one of the required methods stays abstract:
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): """Return the area as a number."""
@abstractmethod def perimeter(self): """Return the perimeter as a number."""
class Square(Shape): def __init__(self, side): self.side = side
def area(self): return self.side**2
try: Square(3)except TypeError: print("TypeError: Square is still abstract")print(sorted(Shape.__abstractmethods__))TypeError: Square is still abstract['area', 'perimeter']Square implemented area but not perimeter, so Square(3) failed, before any method could be called with half a class. The set __abstractmethods__ lists what is still missing. This is the main benefit over duck typing: the error comes at the moment of creation, and it names the missing piece.
Abstract and concrete together
An ABC can mix abstract methods with ordinary ones. The ordinary methods are written once and use the abstract ones, which gives you a fixed procedure with steps the subclasses fill in:
from abc import ABC, abstractmethod
class Report(ABC): @abstractmethod def body(self): """Return the report text."""
def render(self): return f"=== report ===\n{self.body()}\n=== end ==="
class SalesReport(Report): def body(self): return "sales: 42"
print(SalesReport().render())=== report ===sales: 42=== end ===Marking implementations with @override
The @override decorator from the inheritance lesson fits naturally here: it states that a method implements something from the parent, and a type checker confirms that the parent has it. Requires version 3.12 or later
from abc import ABC, abstractmethodfrom typing import override
class Greeter(ABC): @abstractmethod def greet(self, name): """Return a greeting."""
class Formal(Greeter): @override def greet(self, name): return f"Good day, {name}."
print(Formal().greet("Asha"))Good day, Asha.Protocols: interfaces without inheritance
An ABC forces every implementer to inherit from it. That is a problem when the class comes from a library you cannot change, or when you would rather not tie unrelated classes to a shared parent. A protocol describes the required methods, and any class that has them counts as a match, with no inheritance at all. typing.Protocol arrived in Python 3.8, so every supported version has it.
from typing import Protocol
class Drawable(Protocol): def draw(self) -> str: ...
class Circle: def draw(self) -> str: return "()"
class Label: def __init__(self, text): self.text = text
def draw(self) -> str: return f"[{self.text}]"
def render(items: list[Drawable]) -> str: return " ".join(item.draw() for item in items)
print(render([Circle(), Label("hi"), Circle()]))() [hi] ()Three things are new here:
class Drawable(Protocol)declares the shape. The method body is..., the literalEllipsis, which is a conventional “nothing here” for a signature.-> strandlist[Drawable]are type hints, which you met in the type hints lesson. They tell readers and checkers what to expect.CircleandLabelnever mentionDrawable. They match because they have adrawmethod with a compatible signature.
At run time, Python does not check any of this. The script above would run identically with no annotations at all. The value comes from a type checker such as mypy or pyright: pass an object without a draw method to render, and it reports the mistake before the program runs. A protocol is, in effect, a duck-typing promise that tools can verify.
Checking at run time
A normal protocol cannot be used with isinstance. Decorate it with @runtime_checkable when you need that:
import iofrom typing import Protocol, runtime_checkable
@runtime_checkableclass Closeable(Protocol): def close(self) -> None: ...
class Connection: def close(self) -> None: print("closed")
class Brick: pass
print(isinstance(Connection(), Closeable))print(isinstance(Brick(), Closeable))print(isinstance(io.StringIO(), Closeable))TrueFalseTrueThe check is shallow. It confirms that the names exist, and nothing more:
from typing import Protocol, runtime_checkable
@runtime_checkableclass Closeable(Protocol): def close(self) -> None: ...
class Odd: def close(self, a, b, c): # wrong shape, right name return "not what the protocol described"
print(isinstance(Odd(), Closeable))TrueOdd.close takes three required arguments, which does not match the protocol, but isinstance said True because it only looked for the name close. Treat the run-time check as a convenience, and leave the full verification to the type checker.
ABC or Protocol?
| ABC | Protocol | |
|---|---|---|
| Implementers must inherit | yes | no |
| Missing methods detected | at instantiation, by Python | by a type checker |
| Can provide shared concrete methods | yes | not as a goal |
| Works with classes you cannot edit | no | yes |
Choose an ABC when you control the classes and want Python to refuse incomplete ones, or when you have shared behaviour to inherit. Choose a Protocol when you describe what a function needs from its arguments, especially for code that accepts types from several sources. Plain duck typing remains a good default for small programs.
Common pitfalls
Forgetting ABC. Using @abstractmethod in a class that does not inherit from ABC enforces nothing, and you can instantiate it freely. The parent must be ABC, or have ABCMeta as its metaclass.
Expecting a Protocol to be enforced at run time. Calling render with a non-drawable raises AttributeError when .draw() is reached, as with any duck-typed code. Only a type checker reports it earlier.
Over-abstracting. An interface with one implementation, or an ABC for a hierarchy of two classes, adds files and names for no gain. Add the abstraction when the second or third implementation appears.
Putting logic in the abstract method body and not calling it. An abstract method can have a body, but subclasses must call it explicitly with super().method() to run it. Most abstract methods are better left as a docstring.
Design a text-processing plugin system.
Pluginis an ABC with a class attributename = "plugin"and an abstract methodrun(self, text).Upper(nameupper) returns the text in upper case.Reverse(namereverse) returns it reversed.apply_all(plugins, text)passes the text through each plugin in turn, printing<name>: <text>after each step, and returns the final text.
Run apply_all([Upper(), Reverse()], "hello"), print the result, and then show that Plugin() cannot be created by printing Plugin is abstract when it raises TypeError. Expected output:
upper: HELLOreverse: OLLEHOLLEHPlugin is abstractShow solution
from abc import ABC, abstractmethod
class Plugin(ABC): name = "plugin"
@abstractmethod def run(self, text): """Return the transformed text."""
class Upper(Plugin): name = "upper"
def run(self, text): return text.upper()
class Reverse(Plugin): name = "reverse"
def run(self, text): return text[::-1]
def apply_all(plugins, text): for plugin in plugins: text = plugin.run(text) print(f"{plugin.name}: {text}") return text
result = apply_all([Upper(), Reverse()], "hello")print(result)try: Plugin()except TypeError: print("Plugin is abstract")upper: HELLOreverse: OLLEHOLLEHPlugin is abstractThe abstract run guarantees that every concrete plugin has one, and apply_all relies on that. name is an ordinary class attribute that subclasses override. The [::-1] slice makes a reversed copy of the string.
Define class Exporter(Protocol) with one method, export(self, rows: list[dict]) -> str. Write two unrelated classes that match it: CsvExporter, which joins each row’s values with commas, one row per line, and CountExporter, which returns text like 2 rows. Write save(exporter: Exporter, rows: list[dict]) -> None, which prints the result of exporter.export(rows).
Call save with each exporter on [{"id": 1, "name": "pen"}, {"id": 2, "name": "ink"}]. Expected output:
1,pen2,ink2 rowsShow solution
from typing import Protocol
class Exporter(Protocol): def export(self, rows: list[dict]) -> str: ...
class CsvExporter: def export(self, rows: list[dict]) -> str: lines = [",".join(str(v) for v in row.values()) for row in rows] return "\n".join(lines)
class CountExporter: def export(self, rows: list[dict]) -> str: return f"{len(rows)} rows"
def save(exporter: Exporter, rows: list[dict]) -> None: print(exporter.export(rows))
rows = [{"id": 1, "name": "pen"}, {"id": 2, "name": "ink"}]save(CsvExporter(), rows)save(CountExporter(), rows)1,pen2,ink2 rowsNeither exporter mentions Exporter. Both match because they have export with the right signature. If you ran a type checker, it would also accept any third class with the same method, and it would reject one without.
Animal has two abstract methods. Fish implements one, and Dog extends Fish with the other. Write what each pass of the loop prints before you run it.
from abc import ABC, abstractmethod
class Animal(ABC): @abstractmethod def speak(self): pass
@abstractmethod def move(self): pass
class Fish(Animal): def move(self): return "swim"
class Dog(Fish): def speak(self): return "woof"
for cls in [Fish, Dog]: try: animal = cls() print(cls.__name__, animal.speak(), animal.move()) except TypeError: print(cls.__name__, "is abstract")Show solution
Fish is abstractDog woof swimFish implements move but not speak, so one abstract method remains and Fish() raises TypeError. Dog inherits move from Fish and adds speak, which leaves nothing abstract, so it can be created. Abstract methods are inherited as requirements until some class in the chain fills each one.
Summary
- An interface is the set of methods other code relies on. Duck typing leaves it implicit, and ABCs and Protocols make it explicit.
- An ABC inherits from
ABCand marks required methods with@abstractmethod. Python refuses to create an instance while any remain unimplemented. - A
typing.Protocollists the methods a function expects. Classes match by having them, with no inheritance, and a type checker verifies it. @runtime_checkableallowsisinstanceon a protocol, but it only checks that the names exist.@overrideRequires version 3.12 or later can mark the methods that implement an abstract one.- Use an ABC when you control the classes and want enforcement or shared code. Use a Protocol to describe requirements on code you do not control.
- Going further: the advanced lesson Protocols and structural typing goes deeper into how Protocols are matched and checked.