Abstract Base Classes in Depth

· 5 min read · Updated March 14, 2026 · advanced
python abc oop interfaces advanced

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