Intermediate
You are building a plugin system where third-party developers can create their own processors, but you need to guarantee that every plugin implements certain methods. Or you have a team project where different developers build different payment gateways, and you want the code to break loudly at class definition time — not silently at runtime — if someone forgets to implement a required method. This is the exact problem Abstract Base Classes solve.
Python’s abc module lets you define abstract classes that serve as contracts. Any class that inherits from an ABC must implement all abstract methods, or Python raises a TypeError when you try to create an instance. No pip install needed — it is part of the standard library.
In this article, we will start with a quick example showing how ABCs enforce method implementation, then explain when and why you would use them instead of duck typing. We will cover creating abstract methods, abstract properties, combining ABCs with concrete methods, and using the built-in ABCs from collections.abc. We will finish with a real-life plugin system project.
Python ABC Quick Example
# quick_abc.py
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
circle = Circle(5)
print(f"Area: {circle.area():.2f}")
print(f"Perimeter: {circle.perimeter():.2f}")
# This would raise TypeError:
# shape = Shape() # Can't instantiate abstract class
Output:
Area: 78.54
Perimeter: 31.42
The Shape class cannot be instantiated directly because it has abstract methods. Any subclass must implement area() and perimeter(), or Python raises TypeError at instantiation time — not when the method is called. This catches bugs early.
What Are Abstract Base Classes and Why Use Them?
Python is famously a duck-typing language: “if it walks like a duck and quacks like a duck, it is a duck.” This flexibility is powerful, but it creates a problem — you only discover missing methods when the code actually tries to call them, which might be deep in a production workflow.
Abstract Base Classes add voluntary structure to Python’s duck typing. They let you define a contract that says “any class claiming to be a Shape must have these methods.” The check happens at class instantiation time, not method call time.
| Approach | Error Timing | Best For |
|---|---|---|
| Duck typing | Runtime, when method is called | Simple scripts, quick prototypes |
| ABC | Instantiation time | Frameworks, plugin systems, team projects |
| Protocol (typing) | Static analysis only | Type checking without inheritance |
Use ABCs when you control the base class hierarchy and want to enforce a contract. Use Protocols (covered in a separate article) when you want structural subtyping without requiring inheritance.
Creating Abstract Base Classes
To create an ABC, inherit from ABC and decorate methods with @abstractmethod. You can mix abstract and concrete methods in the same class.
# creating_abcs.py
from abc import ABC, abstractmethod
class DataExporter(ABC):
def __init__(self, data):
self.data = data
@abstractmethod
def export(self, filename):
"""Export data to a file. Subclasses must implement this."""
pass
def preview(self, rows=3):
"""Concrete method -- shared by all subclasses."""
print(f"Preview (first {rows} items):")
for item in self.data[:rows]:
print(f" {item}")
def validate(self):
"""Concrete method with shared validation logic."""
if not self.data:
raise ValueError("No data to export")
print(f"Validated: {len(self.data)} records ready")
class CSVExporter(DataExporter):
def export(self, filename):
self.validate()
with open(filename, "w") as f:
for row in self.data:
f.write(",".join(str(v) for v in row.values()) + "\n")
print(f"Exported to {filename}")
class JSONExporter(DataExporter):
def export(self, filename):
import json
self.validate()
with open(filename, "w") as f:
json.dump(self.data, f, indent=2)
print(f"Exported to {filename}")
# Usage
records = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
csv_exp = CSVExporter(records)
csv_exp.preview()
csv_exp.export("people.csv")
print()
json_exp = JSONExporter(records)
json_exp.export("people.json")
Output:
Preview (first 3 items):
{'name': 'Alice', 'age': 30}
{'name': 'Bob', 'age': 25}
Validated: 2 records ready
Exported to people.csv
Validated: 2 records ready
Exported to people.json
The key pattern here is that DataExporter provides shared functionality (preview() and validate()) while forcing subclasses to implement the specific export() logic. This is the Template Method pattern — the base class defines the algorithm skeleton and subclasses fill in the details.
Abstract Properties
# abstract_properties.py
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def fuel_type(self):
pass
@property
@abstractmethod
def max_speed(self):
pass
def describe(self):
print(f"Fuel: {self.fuel_type}, Max Speed: {self.max_speed} km/h")
class ElectricCar(Vehicle):
@property
def fuel_type(self):
return "Electric"
@property
def max_speed(self):
return 200
class DieselTruck(Vehicle):
@property
def fuel_type(self):
return "Diesel"
@property
def max_speed(self):
return 120
car = ElectricCar()
car.describe()
truck = DieselTruck()
truck.describe()
Output:
Fuel: Electric, Max Speed: 200 km/h
Fuel: Diesel, Max Speed: 120 km/h
Stack @property above @abstractmethod to create abstract properties. Subclasses must implement these as properties (not regular methods), ensuring a consistent interface.
Built-in ABCs from collections.abc
Python provides a rich set of ABCs in collections.abc that define standard interfaces for container types. These let you check if an object supports iteration, indexing, or other protocols without checking for specific types.
# collections_abc.py
from collections.abc import Iterable, Sequence, Mapping, Callable
# isinstance checks with ABCs
print(f"list is Iterable: {isinstance([1,2,3], Iterable)}")
print(f"dict is Mapping: {isinstance({'a': 1}, Mapping)}")
print(f"str is Sequence: {isinstance('hello', Sequence)}")
print(f"lambda is Callable: {isinstance(lambda x: x, Callable)}")
print(f"int is Iterable: {isinstance(42, Iterable)}")
# Custom class implementing Iterable
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
current = self.start
while current > 0:
yield current
current -= 1
c = Countdown(3)
print(f"\nCountdown is Iterable: {isinstance(c, Iterable)}")
print(f"Countdown values: {list(c)}")
Output:
list is Iterable: True
dict is Mapping: True
str is Sequence: True
lambda is Callable: True
int is Iterable: False
Countdown is Iterable: True
Countdown values: [3, 2, 1]
The beauty of collections.abc is structural checking — your Countdown class is recognized as Iterable just because it implements __iter__, without explicitly inheriting from Iterable. This is called virtual subclassing.
Real-Life Example: Payment Gateway Plugin System
Let us build a payment processing system where each gateway must implement a standard interface.
# payment_system.py
from abc import ABC, abstractmethod
from datetime import datetime
class PaymentGateway(ABC):
def __init__(self, merchant_id):
self.merchant_id = merchant_id
self.transactions = []
@abstractmethod
def charge(self, amount, currency, customer_id):
"""Process a payment. Returns transaction ID."""
pass
@abstractmethod
def refund(self, transaction_id):
"""Refund a transaction. Returns True if successful."""
pass
@property
@abstractmethod
def gateway_name(self):
pass
def log_transaction(self, tx_type, amount, tx_id):
entry = {
"type": tx_type,
"amount": amount,
"id": tx_id,
"time": datetime.now().strftime("%H:%M:%S"),
"gateway": self.gateway_name
}
self.transactions.append(entry)
print(f" [{entry['gateway']}] {tx_type}: amount={amount}, id={tx_id}")
def get_summary(self):
total = sum(t["amount"] for t in self.transactions if t["type"] == "charge")
refunds = sum(t["amount"] for t in self.transactions if t["type"] == "refund")
print(f" {self.gateway_name}: {len(self.transactions)} transactions, " +
f"charged={total}, refunded={refunds}")
class StripeGateway(PaymentGateway):
@property
def gateway_name(self):
return "Stripe"
def charge(self, amount, currency, customer_id):
tx_id = f"stripe_ch_{id(self)}_{len(self.transactions)}"
self.log_transaction("charge", amount, tx_id)
return tx_id
def refund(self, transaction_id):
self.log_transaction("refund", 0, transaction_id)
return True
class PayPalGateway(PaymentGateway):
@property
def gateway_name(self):
return "PayPal"
def charge(self, amount, currency, customer_id):
tx_id = f"pp_{customer_id}_{len(self.transactions)}"
self.log_transaction("charge", amount, tx_id)
return tx_id
def refund(self, transaction_id):
self.log_transaction("refund", 0, transaction_id)
return True
# Process payments through multiple gateways
gateways = [StripeGateway("merch_001"), PayPalGateway("merch_002")]
print("Processing payments:")
tx1 = gateways[0].charge(99.99, "USD", "cust_alice")
tx2 = gateways[1].charge(49.50, "USD", "cust_bob")
tx3 = gateways[0].charge(149.99, "USD", "cust_charlie")
gateways[0].refund(tx1)
print("\nSummaries:")
for gw in gateways:
gw.get_summary()
Output:
Processing payments:
[Stripe] charge: amount=99.99, id=stripe_ch_140234567_0
[PayPal] charge: amount=49.5, id=pp_cust_bob_0
[Stripe] charge: amount=149.99, id=stripe_ch_140234567_1
[Stripe] refund: amount=0, id=stripe_ch_140234567_0
Summaries:
Stripe: 3 transactions, charged=249.98, refunded=0
PayPal: 1 transactions, charged=49.5, refunded=0
This pattern is powerful because adding a new gateway (say, SquareGateway) only requires implementing three methods and one property. The rest of the infrastructure — logging, summaries, transaction tracking — comes for free from the base class. If someone forgets to implement refund(), Python catches it immediately.
Defining an ABC
An abstract base class declares the contract a subclass must implement. The abc module’s ABC base + @abstractmethod decorator make this enforceable at instantiation time:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key: str, value: bytes) -> None:
...
@abstractmethod
def load(self, key: str) -> bytes:
...
class FileStorage(Storage):
def save(self, key, value):
with open(key, "wb") as f:
f.write(value)
def load(self, key):
with open(key, "rb") as f:
return f.read()
# Works
fs = FileStorage()
fs.save("a.bin", b"hello")
# Won't work — missing implementation
class BrokenStorage(Storage):
def save(self, key, value): pass
# Forgot to implement load()
BrokenStorage()
# TypeError: Can't instantiate abstract class BrokenStorage with abstract method load
The error fires at instantiation, not at first call — much better than a hidden bug that only triggers when something tries to load. ABCs turn “trust the developer” into “the language enforces the contract”.
ABC vs Protocol vs Duck Typing
Python has three ways to express “implements this interface”:
- ABC (nominal): Subclasses must inherit from the ABC. Enforced at runtime.
- Protocol (structural): Any class with the right methods counts, no inheritance needed. Enforced at type-check time only.
- Duck typing: No declaration. Just hope. Caller crashes if the method is missing.
Use ABCs when you need runtime enforcement and a clear inheritance hierarchy. Use Protocols when you want lightweight, mypy-checked interfaces without forcing inheritance (great for plugin systems, third-party integration). Use plain duck typing for one-off scripts where the type-checker overhead isn’t worth it.
from typing import Protocol
class Comparable(Protocol):
def __lt__(self, other: "Comparable") -> bool: ...
# Any class with __lt__ satisfies Comparable, no inheritance needed
def sort_descending(items: list[Comparable]) -> list[Comparable]:
return sorted(items, reverse=True)
Built-in ABCs You Already Use
The collections.abc module has battle-tested ABCs for the standard container types: Iterable, Iterator, Sequence, Mapping, Set, Hashable. You can subclass them to get most methods for free:
from collections.abc import Mapping
class FrozenDict(Mapping):
def __init__(self, data):
self._data = dict(data)
def __getitem__(self, key):
return self._data[key]
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
# Inherited for free: keys(), values(), items(), get(), contains, eq, etc.
fd = FrozenDict({"a": 1, "b": 2})
print("a" in fd, fd.get("a"), list(fd.items()))
Subclassing Mapping gives you 10+ methods for free with just 3 abstract methods implemented. This is the killer feature of ABCs — useful default behavior bundled with the contract.
Registering External Classes
What if you don’t control the class but want it to count as your ABC? register() adds it without modifying source:
from collections.abc import Iterable
class MyExternalThing:
def __iter__(self):
return iter([1, 2, 3])
Iterable.register(MyExternalThing)
print(isinstance(MyExternalThing(), Iterable)) # True
Registration is “this class virtually inherits from the ABC” — useful for adapting third-party libraries to your type system.
Common Pitfalls
- Abstract methods that have bodies.
@abstractmethoddoesn’t prevent you from writing a body — you can callsuper().save(...)from the subclass. But ABCs can’t be instantiated, so the body only runs if explicitly called from a subclass. - Forgetting @classmethod / @staticmethod ordering. Stack the decorators correctly:
@classmethodon top,@abstractmethoddirectly above the method. Reversed order silently breaks the abstract check. - Multiple inheritance MRO confusion. When your subclass inherits from two ABCs, Python’s MRO determines which abstract method counts. Read the MRO carefully or stick to single inheritance.
- Mixing ABC and Protocol in the same hierarchy. Pick one paradigm per interface. Mixing them yields unpredictable runtime/type-check behavior.
- Treating ABCs as just type hints. An ABC enforces at instantiation. A Protocol enforces at type-check. They’re not interchangeable.
FAQ
Q: ABC or Protocol — which should I default to?
A: For new code targeting Python 3.8+, Protocol is usually lighter. For libraries that need to enforce at runtime (plugin systems, framework hooks), ABC.
Q: Can an ABC have non-abstract methods?
A: Yes — and it’s a strong feature. Mix abstract method contracts with default implementations that use those abstract methods. Subclasses implement the abstract parts and inherit the rest.
Q: How do I make a property abstract?
A: @property stacked with @abstractmethod: @property\n@abstractmethod\ndef name(self): .... Same for setters and class methods.
Q: Are ABCs slow?
A: The abstract-method check runs once at instantiation. Method dispatch on ABC subclasses is identical to regular Python — zero runtime cost after the constructor.
Q: When does isinstance() work for unrelated classes?
A: After explicit register(), or if the class implements the structural methods (via __subclasshook__). The collections.abc ABCs use the hook trick — that’s why ANY iterable counts as Iterable.
Wrapping Up
Abstract base classes give Python the “interface” concept without ceremony. Use them when you genuinely need to enforce a contract at instantiation time — plugin systems, framework hooks, base classes you’ll subclass many times. For lighter-weight typing, reach for typing.Protocol instead. The combo of ABCs from collections.abc plus your own bespoke ABCs covers nearly every “I need an interface” use case in Python.