Skip to content

How to Choose Between Abstract Classes and Protocols in Python

Learn when to use Python's Abstract Base Classes (ABCs) for strict inheritance hierarchies with shared implementation versus Protocols for flexible structural typing and duck typing.

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
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

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

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

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.
Navigation