Abstract Base Classes in Depth
If you have used Python for a while, you probably know the basics of ABCs—define a class inheriting from ABC, mark methods with @abstractmethod, and subclasses must implement them. But there is much more to the abc module than this surface-level pattern. This guide takes you deeper into abstract base classes, exploring patterns that separate novice ABC usage from expert-level implementation.
The ABC Metaclass
When you inherit from ABC, your class uses ABCMeta as its metaclass. Understanding this relationship unlocks powerful capabilities:
from abc import ABC, abstractmethod
class MyABC(ABC):
pass
print(type(MyABC).__name__) # ABCMeta
This means ABCs have metaclass powers. You can define __init_subclass__ on an ABC to customize how subclasses are created:
class Base(ABC):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"Subclass {cls.__name__} registered")
class Derived(Base):
pass
# Output: Subclass Derived registered
Abstract Properties
Properties are first-class descriptors in Python, and they can be abstract. This forces subclasses to provide specific property implementations:
from abc import ABC, abstractmethod
from typing import ReadOnly
class Resource(ABC):
@property
@abstractmethod
def name(self) -> str:
"""Every resource must have a name."""
pass
@property
@abstractmethod
def size(self) -> int:
"""Every resource must report its size."""
pass
@property
def summary(self) -> str:
"""Non-abstract: provides default behavior."""
return f"{self.name} ({self.size} bytes)"
class File(Resource):
def __init__(self, name: str, content: bytes):
self._name = name
self._content = content
@property
def name(self) -> str:
return self._name
@property
def size(self) -> int:
return len(self._content)
f = File("test.txt", b"hello world")
print(f.summary) # test.txt (11 bytes)
The combination of @property and @abstractmethod (in either order) works. This enforces that any subclass must define these readable properties.
Abstract Class Methods and Static Methods
ABCs are not limited to instance methods. You can require subclasses to provide class methods or static methods:
from abc import ABC, abstractmethod
class Serializer(ABC):
@classmethod
@abstractmethod
def from_dict(cls, data: dict) -> "Serializer":
"""Deserialize from a dictionary."""
pass
@staticmethod
@abstractmethod
def validate_data(data: dict) -> bool:
"""Validate raw data before deserialization."""
pass
class JSONSerializer(Serializer):
@classmethod
def from_dict(cls, data: dict) -> "JSONSerializer":
return cls(**data)
@staticmethod
def validate_data(data: dict) -> bool:
return isinstance(data, dict)
print(JSONSerializer.validate_data({"key": "value"})) # True
print(JSONSerializer.from_dict({"x": 1})) # <JSONSerializer object>
This pattern is useful for factory methods and validation logic that belongs to the class but must be implemented by each subclass.
Combining Abstract Methods with Default Implementations
Sometimes you want to provide a default implementation while still allowing overrides. Python 3.3+ lets you combine abstract methods with concrete implementations:
from abc import ABC, abstractmethod
class BaseFormatter(ABC):
@abstractmethod
def format(self, value) -> str:
"""Must be implemented by subclass."""
pass
def format_all(self, values: list) -> list:
"""Apply format to all values."""
return [self.format(v) for v in values]
def __call__(self, value) -> str:
"""Convenience: make formatter callable."""
return self.format(value)
class UppercaseFormatter(BaseFormatter):
def format(self, value) -> str:
return str(value).upper()
formatter = UppercaseFormatter()
print(formatter("hello")) # HELLO
print(formatter.format_all(["a", "b", "c"])) # [A, B, C]
The format method is abstract (subclasses must provide it), but format_all and __call__ are concrete and inherited. Subclasses inherit the concrete methods automatically.
Virtual Subclass Registration
The register() method lets you declare any class as a virtual subclass without inheritance:
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self, canvas):
pass
# Register a third-party class as a Drawable
class ThirdPartyGraphic:
def render(self):
print("Rendering...")
Drawable.register(ThirdPartyGraphic)
g = ThirdPartyGraphic()
print(isinstance(g, Drawable)) # True
This is powerful for adapting existing classes to interfaces. However, virtual subclasses bypass the type checking that inheritance provides—you lose some guarantees about interface compliance.
##运行时 Check: init_subclass Instead of ABCs
For simpler cases, __init_subclass__ provides a lighter alternative to ABCs:
class ValidatedBase:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Enforce that subclasses implement "validate"
if "validate" not in cls.__dict__ and not cls.__name__.startswith("_"):
raise TypeError(f"{cls.__name__} must implement validate()")
class GoodChild(ValidatedBase):
def validate(self, value):
return value > 0
# ValidatedBase without validate will fail at definition time
class BadChild(ValidatedBase):
pass # Missing validate()
This pattern gives you similar enforcement to ABCs but with more control over the error messages and conditions.
ABCs vs Protocols: When to Choose Which
This is one of the most common design questions in Python. The short answer:
- Use ABCs when you need formal inheritance, enforced implementation, and type checking with
isinstance() - Use Protocols when you want structural subtyping (duck typing) without inheritance
# ABC approach: explicit interface via inheritance
from abc import ABC, abstractmethod
class Iterator(ABC):
@abstractmethod
def __next__(self):
pass
@abstractmethod
def __iter__(self):
pass
# Protocol approach: structural subtyping
from typing import Protocol
class IteratorProtocol(Protocol):
def __next__(self) -> int:
...
def __iter__(self):
...
With ABCs, isinstance() works naturally. With Protocols, you need typing.cast() or structural checks.
Advanced Pattern: Enforcing Method Signatures
You can combine ABCs with runtime signature checking:
from abc import ABC, abstractmethod
import inspect
class StrictInterface(ABC):
@abstractmethod
def process(self, data: str, options: dict = None):
pass
# Missing optional parameter - will fail at runtime when called
class Implementation(StrictInterface):
def process(self, data: str): # Missing "options" parameter
pass
For stricter enforcement, consider using __init_subclass__ to check signatures at subclass creation time.
Common Anti-Patterns
1. Overusing ABCs: Not every class hierarchy needs abstraction. If there is only ever going to be one implementation, skip the ABC.
2. Forcing implementation of methods that make no sense: If a method does not make sense for a particular subclass, the design is wrong—consider splitting the interface.
3. Virtual subclass abuse: Registering too many unrelated classes as virtual subclasses dilutes the meaning of isinstance() checks.
4. Using ABCs where Protocols would suffice: For simple interface checking without inheritance, Protocols are cleaner.
See Also
- Python Decorators Explained — companion pattern for modifying class and function behavior
type()— The built-in function that is the default metaclass for all classes- Structural Pattern Matching — another advanced Python feature for type-based dispatch
- Protocol Classes — structural subtyping as an alternative to ABCs