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.
Why Use Abstract Base Classes?
Section titled “Why Use Abstract Base Classes?”Without ABCs (no enforcement):
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 itcircle = Circle(5.0)print(circle.area()) # Returns None - silent failure!With ABCs (enforced contracts):
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 classUnderstanding ABCs
Section titled “Understanding ABCs”Basic ABC Structure
Section titled “Basic ABC Structure”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"
# Usagedog = Dog()print(dog.make_sound()) # Woof!print(dog.move()) # Running on four legsprint(dog.sleep()) # Zzz... (inherited)
# Cannot instantiate abstract class# animal = Animal() # TypeError: Can't instantiate abstract classCreating ABCs
Section titled “Creating ABCs”Method 1: Inheriting from ABC
Section titled “Method 1: Inheriting from ABC”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}Method 2: Using ABCMeta
Section titled “Method 2: Using ABCMeta”from abc import ABCMeta, abstractmethod
class PaymentProcessor(metaclass=ABCMeta): """Abstract payment processor using ABCMeta"""
@abstractmethod def process_payment(self, amount: float) -> dict: passAbstract Properties
Section titled “Abstract Properties”You can also define abstract properties:
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"
# Usagecar = Car(120.0)print(car.max_speed) # 120.0print(car.start()) # Car engine startedReal-World Examples
Section titled “Real-World Examples”1. Payment Processing System
Section titled “1. Payment Processing System”from abc import ABC, abstractmethodfrom 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 seamlesslyprocessors = [ 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']}")2. Database Connection Interface
Section titled “2. Database Connection Interface”from abc import ABC, abstractmethodfrom 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 easilydef 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 MySQLprocess_database(postgres_db) # Works with PostgreSQL3. Notification System
Section titled “3. Notification System”from abc import ABC, abstractmethodfrom 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 channelchannels = [ 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}")ABCs vs Protocols
Section titled “ABCs vs Protocols”Python 3.8+ introduced Protocol for structural typing. Here’s when to use each:
Use ABCs when:
Section titled “Use ABCs when:”- 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
Use Protocols when:
Section titled “Use Protocols when:”- 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
from abc import ABC, abstractmethodfrom typing import Protocol
# ABC - enforces implementationclass DrawableABC(ABC): @abstractmethod def draw(self) -> None: pass
class CircleABC(DrawableABC): def draw(self) -> None: # Must implement print("Drawing circle")
# Protocol - structural typingclass DrawableProtocol(Protocol): def draw(self) -> None: ...
class CircleProtocol: def draw(self) -> None: # Implements protocol implicitly print("Drawing circle")
# ABC enforces at instantiationcircle_abc = CircleABC() # Works
# Protocol doesn't enforce - just type checkingcircle_protocol: DrawableProtocol = CircleProtocol() # Type check passesRegistering Classes
Section titled “Registering Classes”You can register classes as implementing an ABC without inheriting:
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 ShapeShape.register(Circle)
# Now isinstance and issubclass workcircle = Circle(5.0)print(isinstance(circle, Shape)) # Trueprint(issubclass(Circle, Shape)) # True
# But Circle can still be instantiated (no abstract methods)circle = Circle(5.0) # Works fineCommon Mistakes to Avoid
Section titled “Common Mistakes to Avoid”Mistake 1: Forgetting @abstractmethod
Section titled “Mistake 1: Forgetting @abstractmethod”# ❌ Bad - method is not abstract, can be instantiatedclass 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 abstractclass GoodShape(ABC): @abstractmethod def area(self) -> float: pass
class GoodCircle(GoodShape): pass # Missing area() implementation
# good_circle = GoodCircle() # TypeError: Can't instantiate abstract classMistake 2: Not Implementing All Abstract Methods
Section titled “Mistake 2: Not Implementing All Abstract Methods”# ❌ Bad - missing abstract method implementationclass 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 methodsclass CompleteCircle(IncompleteShape): def area(self) -> float: return 3.14159 * 5 ** 2
def perimeter(self) -> float: return 2 * 3.14159 * 5
complete_circle = CompleteCircle() # Works!Mistake 3: Using ABCs Unnecessarily
Section titled “Mistake 3: Using ABCs Unnecessarily”# ❌ Bad - ABC when simple inheritance would workclass SimpleData(ABC): @abstractmethod def get_value(self): pass
# ✅ Good - use ABC only when you need to enforce contractsclass SimpleData: def get_value(self): return 42
# Use ABCs when:# - You want to enforce implementation# - You have multiple implementations# - You need polymorphism guaranteesBest Practices
Section titled “Best Practices”- Use ABCs for interfaces - When you need to enforce contracts
- Document abstract methods - Explain what subclasses must implement
- Provide concrete methods - Share common logic in ABC
- Use @abstractmethod - Don’t forget the decorator
- Implement all abstract methods - Subclasses must implement everything
- Consider Protocols - For structural typing instead of inheritance
Key Takeaways
Section titled “Key Takeaways”- 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! 📋