Abstract Base Classes#
Abstract Base Classes (ABCs) define interfaces — contracts that subclasses must fulfill — without providing complete implementations. Python's abc module lets you declare "this method must exist" and fail fast at instantiation time if it doesn't, rather than at call time with NotImplementedError.
This page extends the ABC preview in Classes and OOP Basics and Introdicion to OOPs.
How to use this page
Read after inheritance basics. Related: Polymorphism, Inheritance and Multiple Inheritance, @property Decorators.
At a glance
| Track | Python Advanced → Object Oriented Programming |
| Sections | 12 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Why ABCs exist in Python · Minimal ABC example · @abstractmethod rules (Python 3.10+) · Abstract properties, classmethods, staticmethods · ABC vs NotImplementedError — two patterns · Virtual subclasses — register() · __subclasshook__ — structural isinstance · Collections ABCs — practical everyday use · … (+4 more)
- Why ABCs exist in Python
- Minimal ABC example
@abstractmethodrules (Python 3.10+)- Abstract properties, classmethods, staticmethods
- ABC vs
NotImplementedError— two patterns - Virtual subclasses —
register() __subclasshook__— structural isinstance- Collections ABCs — practical everyday use
- ABC + multiple inheritance
typing.Protocolvsabc.ABC- Interview pattern — plugin architecture
- Interview pattern — strategy via ABC
Why ABCs exist in Python#
Python has no interface keyword. Duck typing works for most code, but sometimes you need:
- Enforced contracts — plugins, frameworks, team APIs
- Fail-fast errors —
TypeErrorat construction, not deep in a call stack - Documentation — explicit list of required methods
- Registration — virtual subclasses without inheritance
| Approach | Enforcement | Flexibility |
|---|---|---|
| Duck typing | None — runtime attribute check | Maximum |
raise NotImplementedError |
Only when method called | Medium |
abc.ABC + @abstractmethod |
At instantiation | Structured |
typing.Protocol (3.8+) |
Static (type checker only) | Structural typing |
Minimal ABC example#
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def get(self, key: str) -> str:
"""Retrieve value by key."""
@abstractmethod
def put(self, key: str, value: str) -> None:
"""Store value by key."""
class MemoryStorage(Storage):
def __init__(self):
self._data: dict[str, str] = {}
def get(self, key: str) -> str:
return self._data[key]
def put(self, key: str, value: str) -> None:
self._data[key] = value
# Storage() # TypeError: Can't instantiate abstract class
ms = MemoryStorage() # OK
ms.put("x", "hello")
ms.get("x") # "hello"
Missing an abstract method:
class BrokenStorage(Storage):
def get(self, key: str) -> str:
return ""
# BrokenStorage() # TypeError: Can't instantiate abstract class BrokenStorage
# # with abstract method put
@abstractmethod rules (Python 3.10+)#
| Rule | Detail |
|---|---|
| Must override | Every @abstractmethod in concrete subclass must be implemented |
| Can have body | Abstract method may include default implementation — subclass still must override |
| Combines with others | @abstractmethod must be innermost when stacked with @classmethod, @staticmethod, @property |
| Instantiation check | Python walks the class __dict__ and MRO; any remaining abstract methods block instantiation |
| Metaclass | ABC sets ABCMeta — you rarely touch this directly |
class Base(ABC):
@abstractmethod
def process(self, data: str) -> str:
return data.upper() # default body — still abstract
class Child(Base):
def process(self, data: str) -> str:
return super().process(data) + "!"
Abstract properties, classmethods, staticmethods#
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float: ...
@classmethod
@abstractmethod
def unit(cls) -> "Shape": ...
@staticmethod
@abstractmethod
def dimensions() -> int: ...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
@property
def area(self) -> float:
return 3.14159 * self.radius ** 2
@classmethod
def unit(cls) -> "Circle":
return cls(1.0)
@staticmethod
def dimensions() -> int:
return 2
Decorator order matters: @abstractmethod is applied last (appears innermost / closest to def):
Not:
ABC vs NotImplementedError — two patterns#
Template method with NotImplementedError#
class Parser:
def parse(self, text: str) -> list:
tokens = self.tokenize(text)
return self.build(tokens)
def tokenize(self, text: str) -> list[str]:
raise NotImplementedError
def build(self, tokens: list[str]) -> list:
raise NotImplementedError
Works, but Parser() succeeds — failure happens on first parse() call.
ABC equivalent#
from abc import ABC, abstractmethod
class Parser(ABC):
def parse(self, text: str) -> list:
tokens = self.tokenize(text)
return self.build(tokens)
@abstractmethod
def tokenize(self, text: str) -> list[str]: ...
@abstractmethod
def build(self, tokens: list[str]) -> list: ...
Parser() fails immediately. Prefer ABCs when the class should never be instantiated directly.
Virtual subclasses — register()#
A class can be considered a subclass without inheriting:
from abc import ABC, abstractmethod
class Closeable(ABC):
@abstractmethod
def close(self) -> None: ...
class FileWrapper:
def close(self) -> None:
print("closing")
Closeable.register(FileWrapper)
issubclass(FileWrapper, Closeable) # True
isinstance(FileWrapper(), Closeable) # True — if all abstract methods exist
Registration is for isinstance/issubclass checks — it does not inject missing methods. FileWrapper must already implement close.
__subclasshook__ — structural isinstance#
For duck-typing-style ABC checks:
from abc import ABC, abstractmethod
class Iterable(ABC):
@abstractmethod
def __iter__(self): ...
@classmethod
def __subclasshook__(cls, subclass):
if cls is Iterable:
if any("__iter__" in b.__dict__ for b in subclass.__mro__):
return True
return NotImplemented
class MyRange:
def __iter__(self):
return iter(range(3))
issubclass(MyRange, Iterable) # True — via __subclasshook__
Built-in ABCs like collections.abc.Sequence use similar hooks.
Collections ABCs — practical everyday use#
The collections.abc module provides ABCs for standard protocols:
from collections.abc import Sequence, Mapping, Iterator
def head(items: Sequence[int]) -> int:
return items[0]
def lookup(table: Mapping[str, int], key: str) -> int:
return table[key]
def consume(it: Iterator[int]) -> list[int]:
return list(it)
| ABC | Requires |
|---|---|
Iterable |
__iter__ |
Iterator |
__iter__ + __next__ |
Sequence |
__getitem__, __len__, index, count |
Mapping |
__getitem__, __len__, __iter__, keys, items, values, get, ... |
MutableMapping |
Mapping + __setitem__, __delitem__ |
Callable |
__call__ |
These integrate with isinstance checks and document expected behavior. See Dunder or Magic Methods.
ABC + multiple inheritance#
ABCs participate normally in MRO:
from abc import ABC, abstractmethod
class Readable(ABC):
@abstractmethod
def read(self) -> str: ...
class Writable(ABC):
@abstractmethod
def write(self, data: str) -> None: ...
class ReadWrite(Readable, Writable):
def read(self) -> str:
return "data"
def write(self, data: str) -> None:
print(data)
rw = ReadWrite() # OK
See Method Resolution Order for diamond inheritance with ABCs.
typing.Protocol vs abc.ABC#
Python 3.8+ structural subtyping for static analysis:
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
def cleanup(resource: SupportsClose) -> None:
resource.close()
| Feature | abc.ABC |
typing.Protocol |
|---|---|---|
| Runtime enforcement | Yes (instantiation) | No (type checker only) |
| Inheritance required | Yes (or register) | No — structural match |
isinstance at runtime |
Yes | With @runtime_checkable only |
| Best for | Frameworks, plugins | Type hints, API documentation |
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsLen(Protocol):
def __len__(self) -> int: ...
isinstance([1, 2], SupportsLen) # True
Use both when you want runtime checks and static typing — ABC for instantiation, Protocol for function signatures.
Interview pattern — plugin architecture#
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float) -> bool: ...
@abstractmethod
def refund(self, transaction_id: str) -> bool: ...
class StripeProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
print(f"Stripe: charged ${amount}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"Stripe: refunded {transaction_id}")
return True
def checkout(processor: PaymentProcessor, amount: float) -> None:
if processor.charge(amount):
print("Payment successful")
checkout(StripeProcessor(), 49.99)
This demonstrates Polymorphism with an explicit contract.
Interview pattern — strategy via ABC#
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list[int]) -> list[int]: ...
class QuickSort(SortStrategy):
def sort(self, data: list[int]) -> list[int]:
return sorted(data) # simplified
class MergeSort(SortStrategy):
def sort(self, data: list[int]) -> list[int]:
return sorted(data) # simplified
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def run(self, data: list[int]) -> list[int]:
return self._strategy.sort(data)
Composition + ABC beats deep inheritance hierarchies for swappable algorithms.
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
Forgetting @abstractmethod |
Base class instantiable with empty body | Decorate every interface method |
| Wrong decorator order | TypeError or method not abstract |
@property / @classmethod above @abstractmethod |
Assuming register() adds methods |
isinstance True but methods missing |
Implement methods first, then register |
| ABC for every class | Over-engineering | Duck typing unless contract needed |
| Confusing Protocol with ABC | No runtime enforcement from Protocol alone | Pick based on runtime vs static needs |
| Partial implementation in MRO | Still abstract if any parent method missing | Implement all abstract methods in concrete class |
Mental model checklist#
- When does Python raise
TypeErrorfor an ABC vsNotImplementedErrorat call time? - How do abstract properties differ from abstract methods syntactically?
- What does
Closeable.register(FileWrapper)actually do? - When would you choose
typing.Protocoloverabc.ABC? - How do
collections.abctypes relate to dunder methods?
What's next#
| Topic | Page |
|---|---|
| Polymorphism in practice | Polymorphism |
| Inheritance design | Inheritance and Multiple Inheritance |
| MRO with ABCs | Method Resolution Order |
| Intermediate OOP | Classes and OOP Basics |