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.