Modern Python OOP: ABC vs Protocol vs Dataclass
Python OOP in 2026 looks different from old Java-style inheritance trees.
Less hierarchy.
Less boilerplate.
More composition.
More flexibility.
If you’re unsure when to use ABC, Protocol, or dataclass, this post keeps it simple.
No theory overload. Just practical guidance.
The Big Picture
There are three separate tools:
- Protocol → defines behavior (what an object can do)
- ABC → defines a family (inheritance + shared logic)
- dataclass → removes boilerplate for data containers
They solve different problems.
Protocol: “If It Can Do The Job, It’s Accepted”
Protocol is modern Python’s preferred abstraction tool.
It says:
I don’t care who you are.
I care what you can do.
Example:
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None: ...
Now any class with send() works.
class EmailNotifier:
def send(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotifier:
def send(self, message: str) -> None:
print(f"SMS: {message}")
No inheritance required.
This gives:
- Loose coupling
- Easier testing
- Clean dependency injection
- Better SOLID compliance
Modern application code prefers Protocol.
ABC: “Join the Family”
ABC is stricter.
It says:
If you want to be part of this system, inherit from me.
Example:
import abc
class BaseNotifier(abc.ABC):
@abc.abstractmethod
def send(self, message: str) -> None:
pass
Subclasses must implement send().
ABC is useful when:
- You want shared default logic
- You need runtime enforcement
- You’re building frameworks or internal platforms
But it creates tighter coupling.
Dataclass: “This Is Mostly Data”
Dataclass removes boilerplate for data-heavy classes.
Instead of writing:
class User:
def __init__(self, name: str):
self.name = name
You write:
from dataclasses import dataclass
@dataclass
class User:
name: str
It automatically generates:
__init____repr____eq__
Use dataclass when:
- The class mostly stores data
- You want readable domain models
- You want less repetitive code
Avoid it for logic-heavy service classes.
What Modern Code Looks Like
Protocol defines behavior:
from typing import Protocol
class PaymentProcessor(Protocol):
def pay(self, amount: float) -> None: ...
Dataclass wires dependencies:
from dataclasses import dataclass
@dataclass(frozen=True)
class PaymentService:
processor: PaymentProcessor
def checkout(self, amount: float) -> None:
self.processor.pay(amount)
Concrete implementations:
class StripeProcessor:
def pay(self, amount: float) -> None:
print(f"Stripe charged {amount}")
class PaypalProcessor:
def pay(self, amount: float) -> None:
print(f"PayPal charged {amount}")
You can swap processors without modifying PaymentService.
That’s clean architecture.
That’s dependency inversion.
That’s modern Python.
Simple Rule of Thumb
If unsure:
- Start with Protocol
- Use dataclass for data objects
- Use ABC only when you truly need shared base logic or runtime enforcement
Avoid deep inheritance trees.
Prefer composition.
Final Mental Model
- Protocol = job description\
- ABC = company membership\
- Dataclass = pre-built container
Modern Python hires based on skills (Protocol),
not family name (ABC).
Keep it simple. Keep it flexible.