How to Choose Between Abstract Classes and Protocols in Python
In languages like Java, "Abstract Class" and "Interface" are strictly different concepts. Python approaches this differently. It uses Abstract Base Classes (ABC) for inheritance and Protocols for structural subtyping, commonly known as duck typing.
This guide helps you choose the right tool for your class design.
Understanding Abstract Base Classes
Use ABCs when you have a family of related classes that share common logic but require specific implementations for some methods. This represents an "is-a" relationship and requires explicit inheritance.
from abc import ABC, abstractmethod
class Animal(ABC):
"""Base class for all animals."""
@abstractmethod
def make_sound(self) -> None:
"""Each animal must implement its own sound."""
pass
def sleep(self) -> None:
"""Shared behavior for all animals."""
print("Zzz...")
class Dog(Animal):
def make_sound(self) -> None:
print("Woof!")
class Cat(Animal):
def make_sound(self) -> None:
print("Meow!")
dog = Dog()
dog.make_sound() # Woof!
dog.sleep() # Zzz...
Key Characteristics of ABCs
- Explicit inheritance is required
- Can contain concrete methods with shared implementation
- Can define abstract properties in addition to methods
- Attempting to instantiate directly raises
TypeError
# This raises TypeError
animal = Animal() # Cannot instantiate abstract class
Using Abstract Properties
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float:
pass
@property
@abstractmethod
def perimeter(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
@property
def area(self) -> float:
return self.width * self.height
@property
def perimeter(self) -> float:
return 2 * (self.width + self.height)
Understanding Protocols
Added in Python 3.8, Protocols enable static duck typing. Classes don't need to inherit from the Protocol. If a class has the required methods, it satisfies the type. This represents a "behaves-like" relationship.
from typing import Protocol
class Drawable(Protocol):
"""Any object that can be drawn."""
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Drawing Circle")
class Button:
def draw(self) -> None:
print("Drawing Button")
def render(item: Drawable) -> None:
"""Accepts any object with a draw method."""
item.draw()
render(Circle()) # Drawing Circle
render(Button()) # Drawing Button
Notice that Circle and Button don't inherit from Drawable. They automatically satisfy the protocol because they implement the required draw method.
Runtime Checking with Protocols
By default, Protocols only work with static type checkers. To enable runtime checking with isinstance(), use the runtime_checkable decorator:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None:
...
class FileHandler:
def close(self) -> None:
print("File closed")
class DatabaseConnection:
def close(self) -> None:
print("Connection closed")
handler = FileHandler()
print(isinstance(handler, Closable)) # True
The runtime_checkable decorator only verifies that methods exist. It does not validate method signatures or return types at runtime.
Combining Multiple Protocols
from typing import Protocol
class Readable(Protocol):
def read(self) -> str:
...
class Writable(Protocol):
def write(self, data: str) -> None:
...
class ReadWritable(Readable, Writable, Protocol):
"""Combines both reading and writing capabilities."""
pass
def copy_data(source: Readable, destination: Writable) -> None:
data = source.read()
destination.write(data)
Practical Example Comparison
Consider building a notification system. Both approaches can work, but they suit different scenarios.
Using Abstract Base Classes
Best when notifications share common functionality:
from abc import ABC, abstractmethod
from datetime import datetime
class Notification(ABC):
def __init__(self, message: str):
self.message = message
self.created_at = datetime.now()
@abstractmethod
def send(self) -> bool:
pass
def log(self) -> None:
print(f"[{self.created_at}] {self.message}")
class EmailNotification(Notification):
def __init__(self, message: str, recipient: str):
super().__init__(message)
self.recipient = recipient
def send(self) -> bool:
self.log()
print(f"Sending email to {self.recipient}")
return True
class SMSNotification(Notification):
def __init__(self, message: str, phone: str):
super().__init__(message)
self.phone = phone
def send(self) -> bool:
self.log()
print(f"Sending SMS to {self.phone}")
return True
Using Protocols
Best when you want flexibility with unrelated classes:
from typing import Protocol
class Sendable(Protocol):
def send(self) -> bool:
...
class EmailClient:
def send(self) -> bool:
print("Email sent via client")
return True
class WebhookTrigger:
def send(self) -> bool:
print("Webhook triggered")
return True
class SlackBot:
def send(self) -> bool:
print("Slack message posted")
return True
def broadcast(channels: list[Sendable]) -> None:
for channel in channels:
channel.send()
broadcast([EmailClient(), WebhookTrigger(), SlackBot()])
Output:
Email sent via client
Webhook triggered
Slack message posted
Comparison Summary
| Feature | Abstract Class (ABC) | Protocol |
|---|---|---|
| Import | from abc import ABC | from typing import Protocol |
| Inheritance | Required (explicit) | Not required (implicit) |
| Shared Code | Yes (concrete methods) | No (signatures only) |
| Runtime Checks | Built-in with isinstance() | Requires @runtime_checkable |
| Philosophy | "This is an Animal" | "This acts like a Drawable" |
| Best For | Class hierarchies | Flexible interfaces |
When to Use Each Approach
Choose Abstract Base Classes when:
- Classes form a logical hierarchy
- You need to share implementation code
- You want to enforce a strict contract
Choose Protocols when:
- Classes are unrelated but share behavior
- You want maximum flexibility
- You're working with third-party classes you cannot modify
Summary
Abstract Base Classes and Protocols serve different purposes in Python's type system.
- ABCs create strict hierarchies with shared implementation, while Protocols enable flexible duck typing without inheritance requirements.
- Understanding when to use each approach leads to cleaner, more maintainable code that accurately models your domain.