Skip to main content

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
No Inheritance Required

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
Runtime Check Limitations

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

FeatureAbstract Class (ABC)Protocol
Importfrom abc import ABCfrom typing import Protocol
InheritanceRequired (explicit)Not required (implicit)
Shared CodeYes (concrete methods)No (signatures only)
Runtime ChecksBuilt-in with isinstance()Requires @runtime_checkable
Philosophy"This is an Animal""This acts like a Drawable"
Best ForClass hierarchiesFlexible interfaces

When to Use Each Approach

Decision Guide

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.