Abstract Base Classes and Interfaces
An abstract base class states what subclasses must implement, and refuses to let an incomplete one be created. It is how Python enforces a contract.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
The problem
class Exporter:
def export(self, data):
raise NotImplementedError("subclasses must implement export")
class CsvExporter(Exporter):
def exprot(self, data): # a typo - the real method is never overridden
return "a,b,c"
e = CsvExporter()
# e.export([]) # NotImplementedError, discovered only when calledThe class was created successfully. The mistake surfaces later, possibly in production, possibly on a rare branch. An abstract base class moves that failure to the moment the object is created.
abc
from abc import ABC, abstractmethod
class Exporter(ABC):
"""Anything that can turn records into text."""
@abstractmethod
def export(self, data):
"""Return the records as a string."""
@abstractmethod
def extension(self):
"""Return the file extension, without a dot."""
class CsvExporter(Exporter):
def export(self, data):
return "\n".join(",".join(str(v) for v in row) for row in data)
def extension(self):
return "csv"
class Incomplete(Exporter):
def export(self, data):
return ""
print(CsvExporter().export([[1, 2], [3, 4]]))
# Exporter() # TypeError: Can't instantiate abstract class Exporter
# Incomplete() # TypeError: ... with abstract method extensionThe error now happens at Incomplete(), naming the missing method. That is a much better place to find out.
Abstract classes can contain real code
from abc import ABC, abstractmethod
class Report(ABC):
def __init__(self, title, rows):
self.title = title
self.rows = rows
@abstractmethod
def format_row(self, row):
"""Format a single row. Subclasses decide how."""
def header(self): # shared, concrete
return f"=== {self.title} ==="
def render(self): # the template method
lines = [self.header()]
lines.extend(self.format_row(row) for row in self.rows)
lines.append(f"({len(self.rows)} rows)")
return "\n".join(lines)
class PlainReport(Report):
def format_row(self, row):
return " ".join(str(value) for value in row)
class PipeReport(Report):
def format_row(self, row):
return "| " + " | ".join(str(value) for value in row) + " |"
rows = [["Meera", 92], ["Arun", 78]]
print(PlainReport("Scores", rows).render())
print()
print(PipeReport("Scores", rows).render())This is the template method pattern: the base class fixes the overall algorithm and leaves specific steps to subclasses. Everything shared is written once.
Abstract properties and other combinations
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def wheels(self):
"""How many wheels this vehicle has."""
@classmethod
@abstractmethod
def from_registration(cls, code):
"""Build an instance from a registration code."""
@staticmethod
@abstractmethod
def category():
"""The vehicle category."""
class Car(Vehicle):
@property
def wheels(self):
return 4
@classmethod
def from_registration(cls, code):
return cls()
@staticmethod
def category():
return "passenger"
print(Car().wheels, Car.category())The order matters: @abstractmethod must be the innermost decorator, directly above the function.
Duck typing with a formal check
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle: # note: it does NOT inherit from Drawable
def draw(self):
return "circle"
class Square:
def draw(self):
return "square"
class Blob:
pass
for shape in [Circle(), Square(), Blob()]:
if isinstance(shape, Drawable):
print(shape.draw())
else:
print(f"{type(shape).__name__} cannot be drawn")A Protocol describes a shape rather than an ancestry. Classes match it simply by having the right methods - which is duck typing, made checkable by tools and, with runtime_checkable, by isinstance. It is the modern alternative to an abstract base class when you do not control the classes involved.
| ABC | Protocol | |
|---|---|---|
| Subclass must inherit | Yes | No |
| Enforced at | Instantiation | Type check time |
| Can share code | Yes | Not usefully |
| Works on third party classes | No | Yes |
| Use when | You own the hierarchy | You describe a capability |
The standard library uses ABCs everywhere
from collections.abc import Sequence, Mapping, Iterable, Sized
print(isinstance([1, 2], Sequence)) # True
print(isinstance("abc", Sequence)) # True
print(isinstance({"a": 1}, Mapping)) # True
print(isinstance({1, 2}, Iterable)) # True
print(isinstance(42, Iterable)) # False
def process(data):
if not isinstance(data, Iterable):
raise TypeError("data must be iterable")
return list(data)from collections.abc import Sequence
class Playlist(Sequence):
"""Implement two methods and inherit the rest of the sequence behaviour."""
def __init__(self, tracks):
self._tracks = list(tracks)
def __getitem__(self, index):
return self._tracks[index]
def __len__(self):
return len(self._tracks)
p = Playlist(["a", "b", "c"])
print(len(p), p[1], "b" in p) # 3 b True
print(list(reversed(p))) # inherited
print(p.index("c"), p.count("a")) # inherited
print([t.upper() for t in p]) # iteration, inheritedBy inheriting from Sequence and writing two methods, the class gained __contains__, __iter__, __reversed__, index and count for free. This is the highest value use of abstract base classes in everyday code.
A worked example
from abc import ABC, abstractmethod
class Storage(ABC):
"""A place notes can be saved to and loaded from."""
@abstractmethod
def save(self, key, value):
"""Store value under key."""
@abstractmethod
def load(self, key):
"""Return the value for key, or None."""
@abstractmethod
def keys(self):
"""Return every stored key."""
# Concrete behaviour built on the abstract methods
def exists(self, key):
return key in self.keys()
def load_all(self):
return {key: self.load(key) for key in self.keys()}
def copy_to(self, other):
for key, value in self.load_all().items():
other.save(key, value)
return other
class MemoryStorage(Storage):
def __init__(self):
self._data = {}
def save(self, key, value):
self._data[key] = value
def load(self, key):
return self._data.get(key)
def keys(self):
return list(self._data)
class FileStorage(Storage):
def __init__(self, folder):
from pathlib import Path
self.folder = Path(folder)
self.folder.mkdir(parents=True, exist_ok=True)
def save(self, key, value):
(self.folder / f"{key}.txt").write_text(value, encoding="utf-8")
def load(self, key):
path = self.folder / f"{key}.txt"
return path.read_text(encoding="utf-8") if path.exists() else None
def keys(self):
return [p.stem for p in self.folder.glob("*.txt")]
memory = MemoryStorage()
memory.save("a", "first note")
memory.save("b", "second note")
print(memory.exists("a"), memory.exists("z"))
print(memory.load_all())
def summarise(store: Storage):
"""Works with any Storage, present or future."""
return f"{type(store).__name__}: {len(store.keys())} notes"
print(summarise(memory))exists, load_all and copy_to were written once and work for every backend. A new storage class needs three methods and inherits the rest.
Common mistakes
- Forgetting to inherit from
ABC, so@abstractmethodhas no effect at all. - Putting
@abstractmethodabove@propertyinstead of below it. - Creating an abstract class with a single implementation, which adds ceremony for nothing.
- Using an ABC where duck typing or a
Protocolwould work. - Assuming
NotImplementedErrorgives the same protection; it fails at call time, not creation time. - Adding a new abstract method to a released base class, breaking every existing subclass.
Best practices
- Use an ABC when several classes must genuinely honour the same contract.
- Put shared behaviour in the base class and leave only the varying steps abstract.
- Inherit from
collections.abctypes to get whole protocols nearly free. - Use
Protocolwhen you are describing a capability rather than owning a hierarchy. - Give every abstract method a docstring stating exactly what an implementation must do.
Practice
- Define an abstract
Notifierwith asendmethod and two concrete implementations. - Show the exact error when a subclass forgets an abstract method.
- Build a class inheriting from
collections.abc.Sequenceand list everything it gained. - Write a template method base class where subclasses supply only one formatting step.
- Define a
Protocoland check three unrelated classes against it withisinstance.
Conclusion
An abstract base class turns "you must implement this" from a comment into an error at instantiation. Use one when several classes share a contract, put the common code in the base, and reach for a Protocol when you only need to describe a capability.