Skip to content

Abstract Base Classes

Define interfaces and enforce contracts with Abstract Base Classes.

Abstract Base Classes (ABCs) in Python allow you to define interfaces that subclasses must implement. They help enforce contracts, enable polymorphism, and make your code more maintainable and self-documenting.

Without ABCs (no enforcement):

without_abc.py
class Shape:
"""Base class for shapes - but nothing enforces implementation"""
def area(self):
"""Should be implemented by subclasses"""
pass # But nothing prevents forgetting to implement it!
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
# Forgot to implement area() - no error until runtime!
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self):
return self.width * self.height
# Problem: Circle doesn't implement area(), but no error until we try to use it
circle = Circle(5.0)
print(circle.area()) # Returns None - silent failure!

With ABCs (enforced contracts):

with_abc.py
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract base class for shapes"""
@abstractmethod
def area(self) -> float:
"""Calculate area - must be implemented by subclasses"""
pass
@abstractmethod
def perimeter(self) -> float:
"""Calculate perimeter - must be implemented by subclasses"""
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.radius
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
# Forgot to implement perimeter() - error at instantiation!
# rectangle = Rectangle(5, 10) # TypeError: Can't instantiate abstract class
basic_abc.py
from abc import ABC, abstractmethod
class Animal(ABC):
"""Abstract base class for animals"""
@abstractmethod
def make_sound(self) -> str:
"""Make a sound - must be implemented"""
pass
@abstractmethod
def move(self) -> str:
"""Move - must be implemented"""
pass
def sleep(self) -> str:
"""Concrete method - can be used as-is or overridden"""
return "Zzz..."
class Dog(Animal):
def make_sound(self) -> str:
return "Woof!"
def move(self) -> str:
return "Running on four legs"
class Cat(Animal):
def make_sound(self) -> str:
return "Meow!"
def move(self) -> str:
return "Sneaking quietly"
# Usage
dog = Dog()
print(dog.make_sound()) # Woof!
print(dog.move()) # Running on four legs
print(dog.sleep()) # Zzz... (inherited)
# Cannot instantiate abstract class
# animal = Animal() # TypeError: Can't instantiate abstract class
inherit_abc.py
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Abstract payment processor"""
@abstractmethod
def process_payment(self, amount: float) -> dict:
"""Process a payment"""
pass
@abstractmethod
def refund(self, transaction_id: str) -> dict:
"""Refund a payment"""
pass
class StripeProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> dict:
return {"status": "success", "gateway": "stripe", "amount": amount}
def refund(self, transaction_id: str) -> dict:
return {"status": "refunded", "transaction_id": transaction_id}
abcmeta.py
from abc import ABCMeta, abstractmethod
class PaymentProcessor(metaclass=ABCMeta):
"""Abstract payment processor using ABCMeta"""
@abstractmethod
def process_payment(self, amount: float) -> dict:
pass

You can also define abstract properties:

abstract_properties.py
from abc import ABC, abstractmethod, abstractproperty
class Vehicle(ABC):
"""Abstract vehicle class"""
@abstractproperty
def max_speed(self) -> float:
"""Maximum speed - must be implemented"""
pass
@abstractmethod
def start(self) -> str:
"""Start the vehicle"""
pass
class Car(Vehicle):
def __init__(self, max_speed: float):
self._max_speed = max_speed
@property
def max_speed(self) -> float:
return self._max_speed
def start(self) -> str:
return "Car engine started"
# Usage
car = Car(120.0)
print(car.max_speed) # 120.0
print(car.start()) # Car engine started
payment_system.py
from abc import ABC, abstractmethod
from typing import Dict, Any
class PaymentProcessor(ABC):
"""Abstract payment processor interface"""
@abstractmethod
def process_payment(self, amount: float, currency: str) -> Dict[str, Any]:
"""Process a payment"""
pass
@abstractmethod
def refund(self, transaction_id: str) -> Dict[str, Any]:
"""Refund a payment"""
pass
@abstractmethod
def get_balance(self) -> float:
"""Get account balance"""
pass
def validate_amount(self, amount: float) -> bool:
"""Concrete method - shared validation logic"""
return amount > 0
class StripeProcessor(PaymentProcessor):
def __init__(self, api_key: str):
self.api_key = api_key
self._balance = 1000.0
def process_payment(self, amount: float, currency: str) -> Dict[str, Any]:
if not self.validate_amount(amount):
raise ValueError("Amount must be positive")
self._balance += amount
return {
"status": "success",
"gateway": "stripe",
"amount": amount,
"currency": currency
}
def refund(self, transaction_id: str) -> Dict[str, Any]:
return {"status": "refunded", "transaction_id": transaction_id}
def get_balance(self) -> float:
return self._balance
class PayPalProcessor(PaymentProcessor):
def __init__(self, client_id: str):
self.client_id = client_id
self._balance = 500.0
def process_payment(self, amount: float, currency: str) -> Dict[str, Any]:
if not self.validate_amount(amount):
raise ValueError("Amount must be positive")
self._balance += amount
return {
"status": "success",
"gateway": "paypal",
"amount": amount,
"currency": currency
}
def refund(self, transaction_id: str) -> Dict[str, Any]:
return {"status": "refunded", "transaction_id": transaction_id}
def get_balance(self) -> float:
return self._balance
# Usage - polymorphism works seamlessly
processors = [
StripeProcessor("sk_test_123"),
PayPalProcessor("paypal_client_456")
]
for processor in processors:
result = processor.process_payment(100.0, "USD")
print(f"{result['gateway']}: {result['status']}")
database_interface.py
from abc import ABC, abstractmethod
from typing import List, Dict, Any
class DatabaseConnection(ABC):
"""Abstract database connection interface"""
@abstractmethod
def connect(self) -> None:
"""Establish database connection"""
pass
@abstractmethod
def disconnect(self) -> None:
"""Close database connection"""
pass
@abstractmethod
def execute_query(self, query: str) -> List[Dict[str, Any]]:
"""Execute a SQL query"""
pass
@abstractmethod
def execute_update(self, query: str) -> int:
"""Execute an update query, return affected rows"""
pass
class MySQLConnection(DatabaseConnection):
def __init__(self, host: str, database: str):
self.host = host
self.database = database
self.connection = None
def connect(self) -> None:
print(f"Connecting to MySQL at {self.host}/{self.database}")
self.connection = {"status": "connected", "type": "mysql"}
def disconnect(self) -> None:
print("Disconnecting from MySQL")
self.connection = None
def execute_query(self, query: str) -> List[Dict[str, Any]]:
print(f"Executing MySQL query: {query}")
return [{"id": 1, "name": "Alice"}]
def execute_update(self, query: str) -> int:
print(f"Executing MySQL update: {query}")
return 1
class PostgreSQLConnection(DatabaseConnection):
def __init__(self, host: str, database: str):
self.host = host
self.database = database
self.connection = None
def connect(self) -> None:
print(f"Connecting to PostgreSQL at {self.host}/{self.database}")
self.connection = {"status": "connected", "type": "postgresql"}
def disconnect(self) -> None:
print("Disconnecting from PostgreSQL")
self.connection = None
def execute_query(self, query: str) -> List[Dict[str, Any]]:
print(f"Executing PostgreSQL query: {query}")
return [{"id": 1, "name": "Bob"}]
def execute_update(self, query: str) -> int:
print(f"Executing PostgreSQL update: {query}")
return 1
# Usage - can swap implementations easily
def process_database(db: DatabaseConnection):
"""Function that works with any DatabaseConnection"""
db.connect()
results = db.execute_query("SELECT * FROM users")
print(f"Results: {results}")
db.disconnect()
mysql_db = MySQLConnection("localhost", "mydb")
postgres_db = PostgreSQLConnection("localhost", "mydb")
process_database(mysql_db) # Works with MySQL
process_database(postgres_db) # Works with PostgreSQL
notification_system.py
from abc import ABC, abstractmethod
from typing import Dict, Any
class NotificationChannel(ABC):
"""Abstract notification channel"""
@abstractmethod
def send(self, recipient: str, message: str) -> Dict[str, Any]:
"""Send a notification"""
pass
@abstractmethod
def validate_recipient(self, recipient: str) -> bool:
"""Validate recipient format"""
pass
class EmailChannel(NotificationChannel):
def send(self, recipient: str, message: str) -> Dict[str, Any]:
if not self.validate_recipient(recipient):
raise ValueError("Invalid email address")
print(f"Sending email to {recipient}: {message}")
return {"status": "sent", "channel": "email", "recipient": recipient}
def validate_recipient(self, recipient: str) -> bool:
return "@" in recipient
class SMSChannel(NotificationChannel):
def send(self, recipient: str, message: str) -> Dict[str, Any]:
if not self.validate_recipient(recipient):
raise ValueError("Invalid phone number")
print(f"Sending SMS to {recipient}: {message}")
return {"status": "sent", "channel": "sms", "recipient": recipient}
def validate_recipient(self, recipient: str) -> bool:
return recipient.isdigit() and len(recipient) == 10
class PushNotificationChannel(NotificationChannel):
def send(self, recipient: str, message: str) -> Dict[str, Any]:
if not self.validate_recipient(recipient):
raise ValueError("Invalid device token")
print(f"Sending push notification to {recipient}: {message}")
return {"status": "sent", "channel": "push", "recipient": recipient}
def validate_recipient(self, recipient: str) -> bool:
return len(recipient) > 20 # Device tokens are long
# Usage - send notifications through any channel
channels = [
EmailChannel(),
SMSChannel(),
PushNotificationChannel()
]
for channel in channels:
try:
result = channel.send("user@example.com", "Hello!")
print(f"Success: {result['channel']}")
except ValueError as e:
print(f"Failed: {e}")

Python 3.8+ introduced Protocol for structural typing. Here’s when to use each:

  • You want to enforce implementation at instantiation
  • You need concrete methods with shared logic
  • You want inheritance-based polymorphism
  • You need to catch missing implementations early
  • You want structural typing (duck typing with type checking)
  • You don’t need to enforce implementation
  • You want more flexibility
  • You’re using type hints primarily
abc_vs_protocol.py
from abc import ABC, abstractmethod
from typing import Protocol
# ABC - enforces implementation
class DrawableABC(ABC):
@abstractmethod
def draw(self) -> None:
pass
class CircleABC(DrawableABC):
def draw(self) -> None: # Must implement
print("Drawing circle")
# Protocol - structural typing
class DrawableProtocol(Protocol):
def draw(self) -> None:
...
class CircleProtocol:
def draw(self) -> None: # Implements protocol implicitly
print("Drawing circle")
# ABC enforces at instantiation
circle_abc = CircleABC() # Works
# Protocol doesn't enforce - just type checking
circle_protocol: DrawableProtocol = CircleProtocol() # Type check passes

You can register classes as implementing an ABC without inheriting:

registering_classes.py
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circle:
"""Circle doesn't inherit from Shape"""
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
# Register Circle as a Shape
Shape.register(Circle)
# Now isinstance and issubclass work
circle = Circle(5.0)
print(isinstance(circle, Shape)) # True
print(issubclass(Circle, Shape)) # True
# But Circle can still be instantiated (no abstract methods)
circle = Circle(5.0) # Works fine
forgetting_abstractmethod.py
# ❌ Bad - method is not abstract, can be instantiated
class BadShape(ABC):
def area(self): # Missing @abstractmethod
pass
class BadCircle(BadShape):
pass # Doesn't implement area, but no error!
bad_circle = BadCircle() # Works - but shouldn't!
# ✅ Good - method is abstract
class GoodShape(ABC):
@abstractmethod
def area(self) -> float:
pass
class GoodCircle(GoodShape):
pass # Missing area() implementation
# good_circle = GoodCircle() # TypeError: Can't instantiate abstract class

Mistake 2: Not Implementing All Abstract Methods

Section titled “Mistake 2: Not Implementing All Abstract Methods”
missing_methods.py
# ❌ Bad - missing abstract method implementation
class IncompleteShape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
class IncompleteCircle(IncompleteShape):
def area(self) -> float:
return 3.14159 * 5 ** 2
# Missing perimeter() - cannot instantiate!
# incomplete_circle = IncompleteCircle() # TypeError
# ✅ Good - implement all abstract methods
class CompleteCircle(IncompleteShape):
def area(self) -> float:
return 3.14159 * 5 ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * 5
complete_circle = CompleteCircle() # Works!
unnecessary_abc.py
# ❌ Bad - ABC when simple inheritance would work
class SimpleData(ABC):
@abstractmethod
def get_value(self):
pass
# ✅ Good - use ABC only when you need to enforce contracts
class SimpleData:
def get_value(self):
return 42
# Use ABCs when:
# - You want to enforce implementation
# - You have multiple implementations
# - You need polymorphism guarantees
  1. Use ABCs for interfaces - When you need to enforce contracts
  2. Document abstract methods - Explain what subclasses must implement
  3. Provide concrete methods - Share common logic in ABC
  4. Use @abstractmethod - Don’t forget the decorator
  5. Implement all abstract methods - Subclasses must implement everything
  6. Consider Protocols - For structural typing instead of inheritance
  • ABCs define interfaces that subclasses must implement
  • @abstractmethod marks methods that must be implemented
  • Cannot instantiate abstract classes directly
  • Early error detection - Errors caught at instantiation
  • Polymorphism - Works seamlessly with inheritance
  • Concrete methods - Can provide shared implementation
  • Protocols - Alternative for structural typing
  • Register classes - Can register without inheriting

Remember: ABCs enforce contracts and define interfaces - ensure your classes implement what they promise! 📋