Python - Abstract Classes (ABC)
Abstract Base Classes provide a way to define interfaces when you want to enforce that derived classes implement particular methods. Unlike informal interfaces relying on duck typing, ABCs make...
Key Insights
- Abstract Base Classes (ABCs) enforce interface contracts in Python, preventing instantiation of incomplete classes and ensuring subclasses implement required methods through the
abcmodule. - ABCs support both abstract methods (must be implemented) and concrete methods (inherited by default), along with abstract properties, class methods, and static methods for comprehensive interface design.
- Use ABCs when building frameworks, plugin systems, or any architecture requiring guaranteed interfaces across multiple implementations—they catch missing implementations at instantiation time rather than runtime.
Understanding Abstract Base Classes
Abstract Base Classes provide a way to define interfaces when you want to enforce that derived classes implement particular methods. Unlike informal interfaces relying on duck typing, ABCs make contracts explicit and fail fast when implementations are incomplete.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
@abstractmethod
def refund_payment(self, transaction_id: str) -> bool:
pass
# This will raise TypeError: Can't instantiate abstract class
# processor = PaymentProcessor()
The ABC class serves as a metaclass helper. Alternatively, use ABCMeta directly:
from abc import ABCMeta, abstractmethod
class PaymentProcessor(metaclass=ABCMeta):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
Both approaches are equivalent; ABC is syntactic sugar for metaclass=ABCMeta.
Implementing Abstract Classes
Subclasses must implement all abstract methods before instantiation:
class StripeProcessor(PaymentProcessor):
def __init__(self, api_key: str):
self.api_key = api_key
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via Stripe")
# Actual Stripe API integration here
return True
def refund_payment(self, transaction_id: str) -> bool:
print(f"Refunding transaction {transaction_id}")
return True
# This works - all abstract methods implemented
processor = StripeProcessor("sk_test_123")
processor.process_payment(99.99)
Missing even one abstract method prevents instantiation:
class IncompleteProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
return True
# Missing refund_payment implementation
# TypeError: Can't instantiate abstract class IncompleteProcessor
# with abstract method refund_payment
# processor = IncompleteProcessor()
Mixing Abstract and Concrete Methods
ABCs can provide default implementations alongside abstract requirements:
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Dict, Any
class DataStore(ABC):
@abstractmethod
def save(self, key: str, value: Any) -> None:
pass
@abstractmethod
def load(self, key: str) -> Any:
pass
# Concrete method with default implementation
def save_with_timestamp(self, key: str, value: Any) -> None:
timestamped_value = {
'data': value,
'timestamp': datetime.now().isoformat()
}
self.save(key, timestamped_value)
# Concrete method that uses abstract methods
def exists(self, key: str) -> bool:
try:
self.load(key)
return True
except KeyError:
return False
class RedisStore(DataStore):
def __init__(self):
self._store: Dict[str, Any] = {}
def save(self, key: str, value: Any) -> None:
self._store[key] = value
def load(self, key: str) -> Any:
return self._store[key]
store = RedisStore()
store.save_with_timestamp('user:123', {'name': 'Alice'})
print(store.exists('user:123')) # True
This pattern allows you to build common functionality in the base class while enforcing implementation of critical operations.
Abstract Properties
Properties can also be abstract, enforcing that subclasses provide specific attributes:
from abc import ABC, abstractmethod
class DatabaseConnection(ABC):
@property
@abstractmethod
def connection_string(self) -> str:
pass
@property
@abstractmethod
def is_connected(self) -> bool:
pass
@abstractmethod
def execute(self, query: str) -> Any:
pass
class PostgresConnection(DatabaseConnection):
def __init__(self, host: str, database: str):
self._host = host
self._database = database
self._connected = False
@property
def connection_string(self) -> str:
return f"postgresql://{self._host}/{self._database}"
@property
def is_connected(self) -> bool:
return self._connected
def execute(self, query: str) -> Any:
if not self.is_connected:
raise RuntimeError("Not connected")
return f"Executing: {query}"
db = PostgresConnection("localhost", "mydb")
print(db.connection_string)
Abstract properties can also have setters:
class ConfigurableService(ABC):
@property
@abstractmethod
def timeout(self) -> int:
pass
@timeout.setter
@abstractmethod
def timeout(self, value: int) -> None:
pass
Abstract Class Methods and Static Methods
Abstract methods work with @classmethod and @staticmethod decorators:
from abc import ABC, abstractmethod
class DocumentParser(ABC):
@classmethod
@abstractmethod
def supported_formats(cls) -> list[str]:
pass
@staticmethod
@abstractmethod
def validate_format(content: str) -> bool:
pass
@abstractmethod
def parse(self, content: str) -> Dict[str, Any]:
pass
class JSONParser(DocumentParser):
@classmethod
def supported_formats(cls) -> list[str]:
return ['.json', '.jsonl']
@staticmethod
def validate_format(content: str) -> bool:
return content.strip().startswith(('{', '['))
def parse(self, content: str) -> Dict[str, Any]:
import json
return json.loads(content)
print(JSONParser.supported_formats())
print(JSONParser.validate_format('{"key": "value"}'))
Virtual Subclasses and Registration
ABCs support virtual subclasses through registration, allowing classes to be recognized as implementations without inheritance:
from abc import ABC, abstractmethod
class Serializable(ABC):
@abstractmethod
def to_dict(self) -> dict:
pass
class ThirdPartyClass:
def to_dict(self) -> dict:
return {'type': 'third_party'}
# Register as virtual subclass
Serializable.register(ThirdPartyClass)
obj = ThirdPartyClass()
print(isinstance(obj, Serializable)) # True
print(issubclass(ThirdPartyClass, Serializable)) # True
Use __subclasshook__ for structural subtyping based on method presence:
class Drawable(ABC):
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'draw') and
callable(subclass.draw))
class Circle:
def draw(self):
print("Drawing circle")
class Square:
pass
print(issubclass(Circle, Drawable)) # True
print(issubclass(Square, Drawable)) # False
Practical Use Case: Plugin System
ABCs excel in plugin architectures where you need guaranteed interfaces:
from abc import ABC, abstractmethod
from typing import List, Dict, Any
class DataExporter(ABC):
@abstractmethod
def export(self, data: List[Dict[str, Any]]) -> str:
pass
@property
@abstractmethod
def file_extension(self) -> str:
pass
def export_to_file(self, data: List[Dict[str, Any]], filename: str) -> None:
content = self.export(data)
full_path = f"{filename}.{self.file_extension}"
with open(full_path, 'w') as f:
f.write(content)
class CSVExporter(DataExporter):
@property
def file_extension(self) -> str:
return "csv"
def export(self, data: List[Dict[str, Any]]) -> str:
if not data:
return ""
headers = ",".join(data[0].keys())
rows = [",".join(str(v) for v in row.values()) for row in data]
return "\n".join([headers] + rows)
class JSONExporter(DataExporter):
@property
def file_extension(self) -> str:
return "json"
def export(self, data: List[Dict[str, Any]]) -> str:
import json
return json.dumps(data, indent=2)
# Plugin registry
exporters: Dict[str, type[DataExporter]] = {
'csv': CSVExporter,
'json': JSONExporter,
}
def export_data(data: List[Dict[str, Any]], format: str, filename: str):
exporter_class = exporters.get(format)
if not exporter_class:
raise ValueError(f"Unknown format: {format}")
exporter = exporter_class()
exporter.export_to_file(data, filename)
data = [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
export_data(data, 'csv', 'output')
export_data(data, 'json', 'output')
This pattern ensures all exporters implement required methods, making the plugin system robust and maintainable. New exporters simply inherit from DataExporter, and the type system enforces compliance at development time.