Skip to main content

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.

Next up in your learning path