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.

ApproachError TimingBest For
Duck typingRuntime, when method is calledSimple scripts, quick prototypes
ABCInstantiation timeFrameworks, plugin systems, team projects
Protocol (typing)Static analysis onlyType 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.

Abstract base class blueprint
abstractmethod says implement me, or TypeError says goodbye.

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.

Abstract methods as gears
Abstract properties — because sometimes you need to enforce attributes, not just methods.

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

Payment gateway plugin system
One abstract class, five payment gateways. Add a sixth without touching existing code.

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.

Connecting interface pieces
TypeError at instantiation beats AttributeError in production. Every time.