Abstract Base Classes in Python
OOP Architecture
Abstract Base Classes
Abstract base classes (ABCs) enforce contracts and provide shared helpers.
The problem: contracts subclasses forget
A base class often exists to say "every subclass must implement these methods." The naive way to express that is to raise NotImplementedError:
class StorageDriver:
def write(self, path, data):
raise NotImplementedError
def read(self, path):
raise NotImplementedError
class S3Driver(StorageDriver):
pass # forgot to implement anything
driver = S3Driver() # constructs fine — the bug is still hidden
driver.write('a.txt', b'hi')
# NotImplementedError
The weakness: the error fires only when the missing method is called. An incomplete subclass can be built, passed around, and shipped; the failure surfaces at runtime, possibly on a rarely-hit code path.
ABC and @abstractmethod
The abc module moves the failure to object creation. Inherit from ABC and mark required methods with @abstractmethod:
from abc import ABC, abstractmethod
class StorageDriver(ABC):
@abstractmethod
def write(self, path: str, data: bytes) -> None: ...
@abstractmethod
def read(self, path: str) -> bytes: ...
StorageDriver()
# TypeError: Can't instantiate abstract class StorageDriver with abstract methods read, write
The same TypeError hits any subclass that leaves a method unimplemented — the incomplete S3Driver above would fail at S3Driver(), not at the first write call. Implement everything and instantiation works:
class MemoryDriver(StorageDriver):
def __init__(self):
self._files = {}
def write(self, path: str, data: bytes) -> None:
self._files[path] = data
def read(self, path: str) -> bytes:
return self._files[path]
driver = MemoryDriver()
driver.write('a.txt', b'hi')
print(driver.read('a.txt')) # -> b'hi'
(Exact wording of the TypeError varies slightly between Python versions; the check itself works everywhere.) If you need an unrelated existing class treated as a subclass without inheritance, StorageDriver.register(SomeClass) makes it a virtual subclass for isinstance checks — though registered classes skip the abstract-method enforcement.
Concrete helpers: the template method
An ABC is not all-abstract. Mixing concrete methods with abstract ones lets the base class own an algorithm's skeleton while subclasses fill in the steps — the template method pattern:
class Exporter(ABC):
def export(self, rows) -> str: # concrete: the shared skeleton
header = self.format_header()
body = [self.format_row(r) for r in rows]
return '\n'.join([header, *body])
@abstractmethod
def format_header(self) -> str: ...
@abstractmethod
def format_row(self, row) -> str: ...
class CsvExporter(Exporter):
def format_header(self):
return 'name,score'
def format_row(self, row):
return f'{row[0]},{row[1]}'
print(CsvExporter().export([('ada', 95), ('bob', 81)]))
# name,score
# ada,95
# bob,81
Every exporter reuses export; only the two formatting steps vary per format. This is where ABCs beat plain interfaces — they carry shared behavior, not just requirements.
Abstract properties
Stack @property on top of @abstractmethod (property outermost) to require a read-only attribute:
class Report(ABC):
@property
@abstractmethod
def title(self) -> str: ...
class SalesReport(Report):
@property
def title(self) -> str:
return 'Q3 sales'
print(SalesReport().title) # -> Q3 sales
Report()
# TypeError: Can't instantiate abstract class Report with abstract method title
ABCs vs Protocols
ABCs enforce contracts nominally: a class participates by inheriting (or registering), and violations fail at instantiation time. typing.Protocol expresses the same contract structurally: any class with matching methods conforms, no inheritance needed, and violations are caught by a static type checker rather than at runtime. Prefer a Protocol when you can't or don't want to touch the implementing classes (third-party types, duck-typed code you're adding type hints to); prefer an ABC when you control the hierarchy and want runtime enforcement plus shared helper methods.
Frequently Asked Questions
Can an ABC have __init__ and normal methods?
Yes. An ABC is an ordinary class plus instantiation checks — it can define __init__, concrete methods, class attributes, and properties, all inherited normally. Subclasses call super().__init__() as usual. Only methods marked @abstractmethod must be overridden before instances can be created.
from abc import ABC, abstractmethod
class Driver(ABC):
def __init__(self, name):
self.name = name # shared state, inherited as-is
@abstractmethod
def connect(self): ...
ABC vs Protocol vs raising NotImplementedError — which one?
NotImplementedError is the weakest: nothing is checked until the missing method is called. An ABC fails fast at instantiation and can bundle shared helpers, but requires inheritance. A Protocol needs no inheritance and is verified statically by a type checker, but does nothing at runtime by default. Rough rule: own hierarchy plus shared code, use an ABC; typing duck-typed or third-party code, use a Protocol.
Do I need ABCs in a small project?
Usually not. With one or two implementations that you also wrote, duck typing plus tests catches the same mistakes with less machinery. ABCs earn their keep when a contract has several implementations, other people (or future you) will write more of them, and a missing method should fail loudly and early.