Timothy had built a library system with multiple storage backends—database storage, file storage, cloud storage. Each backend had the same methods (save_book, load_book, delete_book), but nothing enforced this contract. Developers kept forgetting to implement all methods, leading to runtime crashes.
class DatabaseStorage:
def save_book(self, book):
# Implementation
pass
def load_book(self, book_id):
# Implementation
pass
def delete_book(self, book_id):
# Implementation
pass
class FileStorage:
def save_book(self, book):
# Implementation
pass
def load_book(self, book_id):
# Implementation
pass
# Oops! Forgot delete_book
# No error until runtime when someone calls it
class Library:
def __init__(self, storage):
self.storage = storage
def remove_book(self, book_id):
self.storage.delete_book(book_id) # Crashes if FileStorage!
Margaret found him debugging yet another missing method. "You need contracts," she explained. "Come to the Interface Gallery—where Python enforces that classes implement required methods."
The Problem with Duck Typing
Timothy learned about Python's flexibility and its cost:
# Python's duck typing: "If it walks like a duck..."
def process_storage(storage):
storage.save_book(book) # Assumes save_book exists
storage.load_book(1) # Assumes load_book exists
storage.delete_book(1) # Assumes delete_book exists
# No error until runtime when a method is missing!
# No way to check if an object has all required methods
# Documentation can help, but humans forget
"Python's duck typing is flexible but risky," Margaret explained. "You can pass any object, but you only discover missing methods when code runs. Abstract Base Classes enforce contracts at class definition time."
Abstract Base Classes: Enforcing Contracts
Margaret showed Timothy the ABC module:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save_book(self, book):
"""Save a book to storage"""
pass
@abstractmethod
def load_book(self, book_id):
"""Load a book from storage"""
pass
@abstractmethod
def delete_book(self, book_id):
"""Delete a book from storage"""
pass
# Can't instantiate abstract class
# storage = Storage() # TypeError: Can't instantiate abstract class
# Must implement ALL abstract methods
class DatabaseStorage(Storage):
def save_book(self, book):
print(f"Saving {book} to database")
def load_book(self, book_id):
print(f"Loading book {book_id} from database")
return {"id": book_id, "title": "Book"}
def delete_book(self, book_id):
print(f"Deleting book {book_id} from database")
# This works - all methods implemented
db_storage = DatabaseStorage()
# This fails at instantiation!
class IncompleteStorage(Storage):
def save_book(self, book):
pass
# Missing load_book and delete_book
# Class definition is fine - no error yet
# Error happens HERE when you try to create an instance:
# incomplete = IncompleteStorage()
# TypeError: Can't instantiate abstract class IncompleteStorage
# with abstract methods load_book, delete_book
"Abstract Base Classes define contracts," Margaret explained. "Subclasses must implement all @abstractmethod methods or they can't be instantiated. The error occurs when you try to create an instance, not at class definition time. However, type checkers like mypy catch incomplete implementations at definition time—before the code even runs."
Abstract Methods Can Have Default Implementation
Timothy learned abstract methods could provide defaults:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save_book(self, book):
"""Save a book - subclasses can call super() for logging"""
print(f"[Storage] Saving book: {book.title}")
@abstractmethod
def load_book(self, book_id):
pass
class DatabaseStorage(Storage):
def save_book(self, book):
super().save_book(book) # Call abstract method for logging
print(f"[Database] Writing to database")
def load_book(self, book_id):
return {"id": book_id}
db = DatabaseStorage()
db.save_book(Book("Dune", "Herbert"))
# [Storage] Saving book: Dune
# [Database] Writing to database
"Abstract methods can contain code," Margaret noted. "They still must be overridden, but subclasses can call super() to use the default implementation. This provides shared behavior while enforcing the contract."
Abstract Properties
Margaret showed Timothy how to require properties:
from abc import ABC, abstractmethod
class LibraryItem(ABC):
@property
@abstractmethod
def title(self):
"""Item must have a title property"""
pass
@property
@abstractmethod
def catalog_id(self):
"""Item must have a catalog ID"""
pass
class Book(LibraryItem):
def __init__(self, title, catalog_id):
self._title = title
self._catalog_id = catalog_id
@property
def title(self):
return self._title
@property
def catalog_id(self):
return self._catalog_id
book = Book("Dune", "B-1965-001")
print(book.title) # Works - property implemented
"Combine @property and @abstractmethod to require properties," Margaret explained. "The order matters: @property first, then @abstractmethod."
Abstract Class Methods and Static Methods
Timothy learned abstract methods could be classmethods or staticmethods:
from abc import ABC, abstractmethod
class LibraryItem(ABC):
@classmethod
@abstractmethod
def from_json(cls, json_data):
"""Factory method - subclasses must implement"""
pass
@staticmethod
@abstractmethod
def validate_isbn(isbn):
"""Validation - subclasses must implement"""
pass
@abstractmethod
def get_title(self):
"""Instance method - subclasses must implement"""
pass
class Book(LibraryItem):
def __init__(self, title, isbn):
self.title = title
self.isbn = isbn
@classmethod
def from_json(cls, json_data):
return cls(json_data['title'], json_data['isbn'])
@staticmethod
def validate_isbn(isbn):
return len(isbn) == 13
def get_title(self):
return self.title
# All three abstract method types implemented
book = Book("Dune", "9780441013593")
loaded = Book.from_json({"title": "Foundation", "isbn": "9780553293357"})
print(Book.validate_isbn("9780441013593")) # True
"You can make class methods and static methods abstract," Margaret noted. "The decorator order matters: @classmethod or @staticmethod first, then @abstractmethod. This enforces factory methods, validation methods, or utility methods."
Checking Types with isinstance()
Timothy learned ABCs enabled type checking:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save_book(self, book):
pass
class DatabaseStorage(Storage):
def save_book(self, book):
print("Saving to database")
class FileStorage(Storage):
def save_book(self, book):
print("Saving to file")
db = DatabaseStorage()
file = FileStorage()
# Both are instances of Storage
print(isinstance(db, Storage)) # True
print(isinstance(file, Storage)) # True
# Type hints work with ABCs
def backup_to_multiple(storages: list[Storage]):
for storage in storages:
storage.save_book(book) # Type checker knows this method exists
backup_to_multiple([db, file]) # Type checker approves
"ABCs create a type hierarchy," Margaret noted. "All implementations are instances of the abstract base. Type checkers and isinstance() can verify objects match the contract."
Python's Built-in ABCs: collections.abc
Margaret showed Timothy that Python's standard library used ABCs extensively:
from collections.abc import Sequence, MutableSequence
class BookList(MutableSequence):
"""Custom list-like container for books"""
def __init__(self):
self._books = []
# Must implement these 5 abstract methods:
def __getitem__(self, index):
return self._books[index]
def __setitem__(self, index, value):
self._books[index] = value
def __delitem__(self, index):
del self._books[index]
def __len__(self):
return len(self._books)
def insert(self, index, value):
self._books.insert(index, value)
# MutableSequence ABC provides these methods for free:
# append, extend, pop, remove, reverse, clear, count, index, etc.
books = BookList()
books.append(book1) # Free method from ABC!
books.extend([book2, book3]) # Free method!
books.reverse() # Free method!
print(books.count(book1)) # Free method!
"Python's collections.abc module provides ABCs for common container types," Margaret explained. "Implement a few required methods, get dozens for free. This is how Python's built-in collections work—they inherit from these ABCs."
Virtual Subclasses with register()
Margaret showed Timothy a dangerous escape hatch:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save_book(self, book):
pass
# Third-party class we can't modify
class LegacyStorage:
def save_book(self, book):
print("Legacy save")
# Register as virtual subclass - no inheritance needed
Storage.register(LegacyStorage)
legacy = LegacyStorage()
print(isinstance(legacy, Storage)) # True!
# But register() doesn't enforce the contract!
class BadStorage:
pass # Missing save_book entirely!
Storage.register(BadStorage)
bad = BadStorage() # No error - register bypasses checks!
print(isinstance(bad, Storage)) # True - but save_book missing!
# bad.save_book(book) # AttributeError at runtime!
"The register() method creates virtual subclasses," Margaret warned. "It makes isinstance() return True without inheritance or contract checking. Useful for third-party classes you can't modify, but dangerous—it bypasses all enforcement. Use sparingly and only when you're certain the class fulfills the contract."
Protocols: Duck Typing with Type Checking
Margaret showed Timothy Python 3.8's modern alternative:
from typing import Protocol
class StorageProtocol(Protocol):
def save_book(self, book) -> None:
"""Save a book to storage"""
...
def load_book(self, book_id: int) -> dict:
"""Load a book from storage"""
...
def delete_book(self, book_id: int) -> None:
"""Delete a book from storage"""
...
# No inheritance needed!
class DatabaseStorage:
def save_book(self, book):
print("Saving to database")
def load_book(self, book_id):
return {"id": book_id}
def delete_book(self, book_id):
print("Deleting from database")
# DatabaseStorage doesn't inherit from StorageProtocol
# But it matches the protocol structurally
def use_storage(storage: StorageProtocol):
storage.save_book(book)
db = DatabaseStorage()
use_storage(db) # Type checker approves - structural match!
"Protocols are structural typing," Margaret explained. "Classes don't need to inherit from the protocol. If they have the right methods with the right signatures, they match. It's duck typing that type checkers can verify."
ABC vs Protocol: When to Use Which
Margaret clarified the distinction:
# ABC - Explicit inheritance required
from abc import ABC, abstractmethod
class StorageABC(ABC):
@abstractmethod
def save_book(self, book):
pass
class DatabaseStorage(StorageABC): # Must explicitly inherit
def save_book(self, book):
pass
# isinstance() works at runtime
print(isinstance(DatabaseStorage(), StorageABC)) # True
# Protocol - Structural matching
from typing import Protocol
class StorageProtocol(Protocol):
def save_book(self, book) -> None:
...
class FileStorage: # No inheritance needed
def save_book(self, book):
pass
# isinstance() doesn't work naturally with Protocols
# (unless using runtime_checkable, shown later)
# But type checkers verify structural match
Use ABC when:
- You control all implementations (internal code)
- You want runtime
isinstance()checks - You need shared default implementation (via
super()) - You want explicit "is-a" relationships
Use Protocol when:
- You don't control implementations (third-party, stdlib)
- You want flexibility without inheritance
- You only care about structure, not lineage
- You prefer duck typing with type safety
Runtime Checkable Protocols
Timothy learned protocols could enable runtime checks:
from typing import Protocol, runtime_checkable
@runtime_checkable
class StorageProtocol(Protocol):
def save_book(self, book) -> None:
...
def load_book(self, book_id: int) -> dict:
...
class DatabaseStorage:
def save_book(self, book):
pass
def load_book(self, book_id):
return {}
db = DatabaseStorage()
# Now isinstance() works!
print(isinstance(db, StorageProtocol)) # True
# But only checks method existence, not signatures
class FakeStorage:
def save_book(self): # Wrong signature!
pass
def load_book(self): # Wrong signature!
pass
fake = FakeStorage()
print(isinstance(fake, StorageProtocol)) # True - only checks names!
"@runtime_checkable enables isinstance() with protocols," Margaret cautioned. "But it only checks method names, not signatures. Type checkers are smarter—they verify full signatures."
The Empty Protocol Trap
Margaret warned Timothy about a common mistake:
from typing import Protocol, runtime_checkable
# WRONG - empty protocol matches almost everything!
@runtime_checkable
class EmptyProtocol(Protocol):
pass
class Book:
def __init__(self, title):
self.title = title
book = Book("Dune")
print(isinstance(book, EmptyProtocol)) # True - Book matches!
# Empty protocols are useless for type checking
# They accept anything
# RIGHT - always define at least one method
@runtime_checkable
class Saveable(Protocol):
def save(self) -> None:
"""Must have save method"""
...
print(isinstance(book, Saveable)) # False - Book lacks save()
class SaveableBook:
def save(self):
print("Saving...")
print(isinstance(SaveableBook(), Saveable)) # True - has save()
"Always define at least one method in a protocol," Margaret emphasized. "Empty protocols are meaningless—they match everything. A protocol should specify required behavior, not be an empty shell."
Multiple Abstract Methods
Margaret showed Timothy interfaces with multiple requirements:
from abc import ABC, abstractmethod
class Searchable(ABC):
@abstractmethod
def search(self, query: str):
"""Search for items"""
pass
class Sortable(ABC):
@abstractmethod
def sort_by(self, field: str):
"""Sort items by field"""
pass
class Filterable(ABC):
@abstractmethod
def filter_by(self, predicate):
"""Filter items"""
pass
# Combine multiple ABCs
class Catalog(Searchable, Sortable, Filterable):
def __init__(self):
self.books = []
def search(self, query):
return [b for b in self.books if query in b.title]
def sort_by(self, field):
return sorted(self.books, key=lambda b: getattr(b, field))
def filter_by(self, predicate):
return [b for b in self.books if predicate(b)]
catalog = Catalog()
print(isinstance(catalog, Searchable)) # True
print(isinstance(catalog, Sortable)) # True
print(isinstance(catalog, Filterable)) # True
"Multiple inheritance with ABCs creates rich contracts," Margaret explained. "Each ABC defines one capability. Combine them to specify complex requirements."
Concrete Methods in ABCs
Timothy learned ABCs could have regular methods too:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save_book(self, book):
pass
@abstractmethod
def load_book(self, book_id):
pass
# Concrete method - available to all subclasses
def save_multiple(self, books):
for book in books:
self.save_book(book) # Uses abstract method
# Concrete method with utility
def validate_book(self, book):
if not hasattr(book, 'title'):
raise ValueError("Book must have title")
return True
class DatabaseStorage(Storage):
def save_book(self, book):
self.validate_book(book) # Use concrete method
print(f"Saving {book.title}")
def load_book(self, book_id):
return {"id": book_id}
db = DatabaseStorage()
db.save_multiple([book1, book2, book3]) # Concrete method works!
"ABCs can mix abstract and concrete methods," Margaret noted. "Abstract methods define the contract. Concrete methods provide shared functionality that uses the abstract methods."
Real-World Example: Plugin System
Margaret demonstrated a practical pattern:
from abc import ABC, abstractmethod
class BookProcessor(ABC):
"""Base class for book processing plugins"""
@abstractmethod
def process(self, book) -> dict:
"""Process a book and return metadata"""
pass
@abstractmethod
def get_name(self) -> str:
"""Return processor name"""
pass
# Concrete helper method
def validate_input(self, book):
if not hasattr(book, 'title'):
raise ValueError(f"{self.get_name()}: Invalid book")
class ISBNExtractor(BookProcessor):
def process(self, book):
self.validate_input(book)
# Extract ISBN from book
return {"isbn": book.isbn}
def get_name(self):
return "ISBN Extractor"
class SummaryGenerator(BookProcessor):
def process(self, book):
self.validate_input(book)
# Generate summary
return {"summary": f"Summary of {book.title}"}
def get_name(self):
return "Summary Generator"
class ProcessingPipeline:
def __init__(self):
self.processors: list[BookProcessor] = []
def register(self, processor: BookProcessor):
self.processors.append(processor)
def process_book(self, book):
results = {}
for processor in self.processors:
results[processor.get_name()] = processor.process(book)
return results
# Build pipeline
pipeline = ProcessingPipeline()
pipeline.register(ISBNExtractor())
pipeline.register(SummaryGenerator())
# Process books - all processors match contract
metadata = pipeline.process_book(book)
"Plugin systems benefit from ABCs," Margaret explained. "The base class defines the plugin interface. Each plugin implements the contract. The system can work with any conforming plugin without knowing specifics."
When NOT to Use ABCs
Margaret emphasized knowing when ABCs were overkill:
# DON'T create ABCs for everything - this is overkill:
from abc import ABC, abstractmethod
class Named(ABC):
@abstractmethod
def get_name(self):
pass
class Book(Named):
def __init__(self, title):
self.title = title
def get_name(self):
return self.title
# Better - just use a regular class or duck typing:
class Book:
def __init__(self, title):
self.title = title
def get_name(self):
return self.title
# Functions work with any object that has get_name
def print_name(obj):
print(obj.get_name()) # Duck typing - no ABC needed
Use ABCs when:
- Multiple implementations exist or are planned
- Contract enforcement is critical for correctness
- Building plugin/extension systems
- Need shared default behavior via concrete methods
- Want explicit "is-a" relationships
Don't use ABCs when:
- Only one implementation exists
- Simple duck typing suffices
- The interface is obvious and unlikely to vary
- Over-engineering for a simple problem
- Adding unnecessary complexity
"ABCs are tools, not requirements," Margaret cautioned. "They add value in systems with multiple implementations and strict contracts. For simple cases, regular classes or duck typing are clearer and more Pythonic."
Timothy's Interface Wisdom
Through exploring the Interface Gallery, Timothy learned essential principles:
Abstract Base Classes enforce contracts: Subclasses must implement all @abstractmethod methods.
Can't instantiate abstract classes: Error occurs at instantiation time, not class definition time.
Type checkers catch errors earlier: mypy finds incomplete implementations before runtime.
Abstract methods can have default implementations: Subclasses can call super() for shared behavior.
@property and @abstractmethod combine: Require properties in subclasses—@property first, then @abstractmethod.
@classmethod and @staticmethod can be abstract: Enforce factory methods and utilities—decorator first, then @abstractmethod.
collections.abc provides built-in ABCs: Implement required methods, get dozens free (Sequence, MutableSequence, etc.).
register() creates virtual subclasses: Makes isinstance() work without inheritance—useful but bypasses enforcement.
isinstance() works with ABCs: Check if objects fulfill abstract contracts at runtime.
Protocols are structural typing: Match by structure, not inheritance.
Protocols don't require inheritance: If methods match, type checkers approve.
@runtime_checkable enables isinstance(): But only checks method names, not signatures.
Empty protocols are useless: Always define at least one method in a protocol.
ABCs are for controlled implementations: When you own all the code and need strict contracts.
Protocols are for flexibility: When you don't control implementations or want duck typing.
ABCs can mix abstract and concrete methods: Define contract with abstract, provide utilities with concrete.
Multiple ABCs combine capabilities: Create rich interfaces through multiple inheritance.
Use ABCs for plugin systems: Enforce plugin contracts at definition time.
Don't overuse ABCs: Simple cases work better with duck typing or regular classes.
Type checkers verify both ABCs and Protocols: Catch missing methods before runtime.
Python's Contract Enforcement Mechanisms
Timothy had discovered Python's contract enforcement mechanisms—Abstract Base Classes for explicit inheritance hierarchies and Protocols for structural typing.
The Interface Gallery revealed that Python could be both flexible and safe—ABCs caught incomplete implementations at instantiation time (or earlier with type checkers), while Protocols enabled type-checked duck typing without inheritance.
He learned that abstract methods could be instance methods, class methods, or static methods, and that Python's own collections.abc used these patterns extensively.
He discovered the dangerous register() escape hatch for virtual subclasses and the trap of empty protocols that matched everything.
Most importantly, he learned when NOT to use ABCs—avoiding over-engineering when simple duck typing sufficed.
He could now design systems with guaranteed contracts, whether through explicit inheritance or structural matching, ensuring that objects fulfilled their promises before code ever ran—while keeping the design simple and Pythonic when complexity wasn't warranted.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)