21 Python Protocol vs ABC - Details
I’ve known ABC, but never heard of Protocol in Python. Please explain and compare them using the following examples:
21.0.1 ABC
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send_notification(self, message: str) -> None:
pass
class EmailNotifier(Notifier):
def send_notification(self, message: str) -> None:
print(f"Sending email: {message}")
class SMSNotifier(Notifier):
def send_notification(self, message: str) -> None:
print(f"Sending SMS: {message}")
class NotificationService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def notify(self, message: str) -> None:
self.notifier.send_notification(message)
# Usage
email_notifier = EmailNotifier()
email_service = NotificationService(email_notifier)
email_service.notify("Hello via email")21.0.2 Protocol
from typing import Protocol
class Notifier(Protocol):
def send_notification(self, message: str) -> None:
...
class EmailNotifier: # Note: no explicit inheritance
def send_notification(self, message: str) -> None:
print(f"Sending email: {message}")
class SMSNotifier: # Note: no explicit inheritance
def send_notification(self, message: str) -> None:
print(f"Sending SMS: {message}")
class NotificationService:
def __init__(self, notifier: Notifier): # Still able to use type hinting
self.notifier = notifier
def notify(self, message: str) -> None:
self.notifier.send_notification(message)
# Usage
sms_notifier = SMSNotifier()
sms_service = NotificationService(sms_notifier)
sms_service.notify("Hello via SMS")Great question! Let me explain the key differences between ABC and Protocol, which represent two different approaches to polymorphism in Python.
21.1 Core Difference
ABC (Abstract Base Class) → Nominal Typing (inheritance-based) Protocol → Structural Typing (duck typing with type hints)
ABC Approach:
┌─────────────┐
│ Notifier │ (ABC)
│ (abstract) │
└──────┬──────┘
│
│ explicit inheritance
│
┌───┴────┬─────────┐
│ │ │
┌──▼───┐ ┌─▼──┐ ┌───▼────┐
│Email │ │SMS │ │Push │
│ │ │ │ │ │
└──────┘ └────┘ └────────┘
Protocol Approach:
┌─────────────┐
│ Notifier │ (Protocol)
│ (interface)│
└─────────────┘
"If it walks like a duck..."
┌────────┬─────────┬──────────┐
│ │ │ │
┌──▼───┐ ┌─▼──┐ ┌───▼────┐ ┌──▼──────┐
│Email │ │SMS │ │Push │ │Third │
│ │ │ │ │ │ │Party │
└──────┘ └────┘ └────────┘ └─────────┘
(no changes
needed!)
21.2 Detailed Comparison
21.2.1 1. Inheritance Requirement
ABC: - Requires explicit inheritance - classes MUST inherit from the ABC - Runtime enforcement - Python checks inheritance at instantiation
class EmailNotifier(Notifier): # Must inherit
def send_notification(self, message: str) -> None:
print(f"Sending email: {message}")Protocol: - No inheritance needed - classes are compatible if they have the right methods - Static type checking only - mypy/pyright check at development time
class EmailNotifier: # No inheritance!
def send_notification(self, message: str) -> None:
print(f"Sending email: {message}")21.2.2 2. Enforcement Timing
ABC:
Development → Runtime
│
└─→ TypeError if abstract methods not implemented
Protocol:
Development → Type Checker (mypy/pyright)
│
└─→ Type error if methods don't match
Runtime → No checking (duck typing)
21.2.3 3. Working with Third-Party Code
ABC - Problematic:
# Imagine this is from a third-party library you can't modify
class ThirdPartyLogger:
def send_notification(self, message: str) -> None:
print(f"Logging: {message}")
# This WON'T work - not inherited from Notifier ABC
service = NotificationService(ThirdPartyLogger()) # Runtime error!
# You'd need to create a wrapper:
class LoggerAdapter(Notifier):
def __init__(self):
self._logger = ThirdPartyLogger()
def send_notification(self, message: str) -> None:
self._logger.send_notification(message)Protocol - Just Works:
# Same third-party code
class ThirdPartyLogger:
def send_notification(self, message: str) -> None:
print(f"Logging: {message}")
# This WORKS! Type checker is happy
service = NotificationService(ThirdPartyLogger()) # ✓21.2.4 4. Runtime Behavior
# ABC - Runtime checking
class IncompleteNotifier(Notifier): # Missing send_notification
pass
notifier = IncompleteNotifier() # TypeError at runtime!
# TypeError: Can't instantiate abstract class IncompleteNotifier
# Protocol - No runtime checking
class IncompleteNotifier: # Missing send_notification
pass
notifier = IncompleteNotifier() # No error!
service = NotificationService(notifier) # No error!
service.notify("test") # AttributeError here!21.2.5 5. Type Checker Integration
Both work with type checkers, but differently:
# With ABC
def process(notifier: Notifier): # Must be Notifier subclass
notifier.send_notification("Hi")
# With Protocol
def process(notifier: Notifier): # Any object with send_notification
notifier.send_notification("Hi")21.3 When to Use Each
21.3.1 Use ABC When:
- You control all implementations - internal project code
- You want runtime enforcement - catch errors immediately
- You want shared behavior - ABCs can have concrete methods
from abc import ABC, abstractmethod
class DataProcessor(ABC):
@abstractmethod
def process(self, data: str) -> str:
pass
# Concrete method all subclasses get
def validate(self, data: str) -> bool:
return len(data) > 0
# Template method pattern
def run(self, data: str) -> str:
if not self.validate(data):
raise ValueError("Invalid data")
return self.process(data)21.3.2 Use Protocol When:
- Working with third-party code - can’t modify to inherit
- Want flexibility - structural typing is more flexible
- Library/framework design - users don’t need to inherit
- Following “duck typing” philosophy - Pythonic approach
from typing import Protocol
# Users can implement this without knowing about your library
class Serializable(Protocol):
def to_dict(self) -> dict:
...
# Works with ANY object that has to_dict method
def save(obj: Serializable):
data = obj.to_dict()
# save logic...21.4 Practical Example: Real-World Scenario
Scenario: You’re building a notification system that needs to work with:
- Your own notification classes
- Third-party services (Twilio, SendGrid)
- Future integrations (unknown providers)
# Protocol approach (recommended)
from typing import Protocol
class Notifier(Protocol):
def send_notification(self, message: str) -> None:
...
# Your code
class EmailNotifier:
def send_notification(self, message: str) -> None:
print(f"Email: {message}")
# Third-party (can't modify)
import twilio
class TwilioClient: # From external library
def send_notification(self, message: str) -> None:
# Twilio's implementation
pass
# Both work seamlessly!
def broadcast(notifiers: list[Notifier], message: str):
for notifier in notifiers:
notifier.send_notification(message)
broadcast([EmailNotifier(), TwilioClient()], "Hello!")21.5 Summary Table
| Feature | ABC | Protocol |
|---|---|---|
| Inheritance Required | ✓ Yes | ✗ No |
| Runtime Checking | ✓ Yes | ✗ No |
| Third-party Integration | ✗ Hard | ✓ Easy |
| Shared Behavior | ✓ Yes | ✗ No |
| Philosophy | Nominal Typing | Structural Typing |
| Python Style | Traditional OOP | Duck Typing |
21.6 My Recommendation
For your radiology AI work where you might integrate with various medical imaging libraries and APIs, Protocol is often better because:
- Medical imaging libraries (SimpleITK, pydicom, etc.) won’t inherit from your classes
- You want flexibility to work with different vendor APIs
- Python community is moving toward Protocols for interfaces
Use ABC when building core internal hierarchies where you control everything and want runtime safety.