ABC (Abstract Base Classes)
Imagine you are designing a set of class hierarchies where you have a parent class with an "abstract" method that needs to be implemented by subclasses to work.
Generally, when you do this, you will use a NotImplementedError in the methods. However there are two major problems:
- Declaring the class succeeds even when certain subclass required methods are not implemented.
- Calling that not-yet implemented method of your subclass fails because it wasn't caught at instantiation-time.
The most common example is something like this:
import abc
class StorageService:
@abc.abstractmethod
def save(self, key, value):
raise NotImplementedError("Subclasses must implement 'save'")
@abc.abstractmethod
def load(self, key):
raise NotImplementedError("Subclasses must implement 'load'")
class InMemoryStorage(StorageService):
def save(self, key, value):
print(f"[InMemory] Saving {key} -> {value}")
# stores it somewhere in memory (fake impl for demo)
self._data = {key: value}
# Instantiation succeeds
store = InMemoryStorage()
store.save("user:1", {\"name\": \"Alex\"})
# But calling the missing method fails only at runtime
print(store.load("user:1")) # NotImplementedError
In the above example, you can still create InMemoryStorage
even though
load() isn't defined. Only when another engineer (or your API
call) tries load() do you hit a
NotImplementedError — potentially in production—do you realize
this problem.
The Solution: ABC with ABCMeta
To resolve this, use the built-in abc.ABCMeta metaclass from
abc
to define abstract classes. This basically prevents you from instantiating
a class at creation time with a TypeError, rather than
failing later.
import abc
from abc import ABC, abstractmethod
class StorageService(ABC): # or metaclass=abc.ABCMeta
@abstractmethod
def save(self, key, value):
"""Save a value by key"""
pass
@abstractmethod
def load(self, key):
"""Load a value by key"""
pass
class InMemoryStorage(StorageService):
def save(self, key, value):
print(f"[InMemory] Saving {key} -> {value}")
self._data = {key: value}
# Oops: still forgot load()!
# Instantiation FAILS immediately
store = InMemoryStorage()
# TypeError: Can't instantiate abstract class InMemoryStorage with abstract method load
Now your instantiations will be safe! The TypeError happens at
the moment you try to create an instance, not later when you call the missing
method. This catches errors during development rather than in production.
Key Benefits
- Early error detection: Missing implementations are caught at instantiation time, not runtime
- Clear contracts: Abstract methods explicitly define what subclasses must implement
- Better IDE support: Most IDEs will warn about missing abstract method implementations
- Type checker friendly: Tools like mypy understand and enforce abstract base classes
Questions or feedback? Feel free to reach out!