Skip to content

Python · Object-Oriented Programming

Python ABC vs Protocol — Nominal vs Structural Typing Explained

6 min read Updated 2026-06-21 Share:

Practice ABC vs Protocol — Nominal vs Structural Typing interview questions

Two ways to define an interface in Python

Python is famous for duck typing — "if it walks like a duck and quacks like a duck, it's a duck." But modern Python gives you two formal tools for defining interfaces on top of that: Abstract Base Classes (ABCs) and typing.Protocol. They look similar at a glance but rest on opposite philosophies — nominal subtyping vs structural subtyping — and that difference drives every practical choice between them.

The core distinction

ABCs use nominal (explicit) subtyping: a class satisfies the interface only if it explicitly declares class MyClass(MyABC). Python enforces this at runtime by refusing to instantiate classes that miss abstract methods.

Protocols use structural (implicit) subtyping: a class satisfies the interface if it simply has the right methods and attributes — no inheritance declaration required. Type checkers like mypy enforce this statically, at lint/check time.

Quick-reference comparison

abc.ABC + @abstractmethodtyping.Protocol
Subtyping styleNominal — must class Foo(MyABC)Structural — matching methods is enough
Explicit registration?Yes (inherit or register())No — implicit via shape
Runtime enforcement?Yes — TypeError on missing methodsOnly with @runtime_checkable
Static type checking?YesYes (primary use case)
isinstance() works?YesOnly with @runtime_checkable
Shared implementation?Yes — concrete mixin methodsNo — Protocols are pure interface
Third-party classes?Via register() hackYes — if they have the right shape
collections.abc integration?YesSeparate
Best forFamilies of related classes you controlDefining callable/structural contracts, typing third-party code
Added inPython 2.6+ (abc module)Python 3.8+ (typing.Protocol)

ABCs — explicit contracts with runtime enforcement

An ABC requires that every concrete subclass explicitly inherits from it and implements all abstract methods. The enforcement happens at instantiation time — not at definition time — which catches missing methods early.

from abc import ABC, abstractmethod

class Serializer(ABC):
    @abstractmethod
    def serialize(self, data: dict) -> str: ...

    @abstractmethod
    def deserialize(self, text: str) -> dict: ...

    def round_trip(self, data: dict) -> dict:    # concrete mixin method
        return self.deserialize(self.serialize(data))

class JsonSerializer(Serializer):
    def serialize(self, data):
        import json; return json.dumps(data)

    def deserialize(self, text):
        import json; return json.loads(text)

Serializer()         # TypeError — can't instantiate abstract class
JsonSerializer()     # OK — all abstract methods implemented

# Works even without explicit subclassing if you use register():
class ThirdPartySerializer:
    def serialize(self, data): ...
    def deserialize(self, text): ...

Serializer.register(ThirdPartySerializer)
isinstance(ThirdPartySerializer(), Serializer)   # True — registered

collections.abc provides battle-tested ABCs for standard container protocols: Iterable, Sequence, Mapping, Callable, and many more. Subclassing these gives you free mixin implementations (e.g. subclass Sequence with just __getitem__ and __len__ and get __contains__, __iter__, index, count for free).

Use ABCs when you have a tight family of classes that must share a contract and you control the class hierarchy — and when you want clear runtime errors at instantiation.

Deep dive: Abstract Base Classes & Protocols interview questions

Protocols — structural typing for duck typing with type safety

typing.Protocol lets you define an interface that any class satisfies simply by having the right methods and attributes — no inheritance required. This is how duck typing works at the type-checker level: a class "is a" Drawable if it has a draw() method, full stop.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:                          # no inheritance from Drawable
    def draw(self) -> None:
        print("drawing circle")

class Square:                          # also has draw() — also satisfies Drawable
    def draw(self) -> None:
        print("drawing square")

def render(shape: Drawable) -> None:   # type checker accepts any matching type
    shape.draw()

render(Circle())    # OK — Circle has draw()
render(Square())    # OK — Square has draw()
render("hello")     # mypy error — str has no draw()

Neither Circle nor Square inherits from Drawable. The type checker accepts them purely because their shape matches. At runtime there's no check unless you add @runtime_checkable:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...

isinstance(Circle(), Drawable)   # True — runtime shape check
isinstance("hello", Drawable)    # False

Note: @runtime_checkable only checks for method/attribute presence, not signatures.

Use Protocols when:

  • You're writing a function that accepts any class with a given shape, including third-party classes you can't modify.
  • You want duck-typed flexibility with static type safety.
  • The relationship is a capability ("can be drawn") rather than a family ("is a Shape").

Deep dive: Generics & Protocols interview questions

The key practical difference — third-party classes

The most important real-world difference: you can't make a third-party class inherit from your ABC (without register(), which is a workaround). But any third-party class that happens to have the right methods automatically satisfies a Protocol.

import pandas as pd

# ABC: pandas DataFrame does NOT subclass your ABC — won't work without register()
class Tabular(ABC):
    @abstractmethod
    def to_csv(self) -> str: ...

isinstance(pd.DataFrame(), Tabular)   # False — unless registered manually

# Protocol: DataFrame has to_csv() — it automatically satisfies the Protocol
from typing import Protocol

class Tabular(Protocol):
    def to_csv(self) -> str: ...

def export(data: Tabular) -> None:
    print(data.to_csv())

export(pd.DataFrame())    # type checker: OK  (DataFrame has to_csv)

This is why modern Python typing favours Protocols for library code and public APIs.

When to share implementation — ABCs win

Protocols are pure interface — no concrete methods. If you need shared implementation alongside the contract, use an ABC:

class Animal(ABC):
    @abstractmethod
    def speak(self) -> str: ...

    def describe(self) -> str:         # concrete mixin — shared by all subclasses
        return f"I am a {type(self).__name__} and I say {self.speak()}"

class Dog(Animal):
    def speak(self) -> str: return "woof"

Dog().describe()   # "I am a Dog and I say woof"

The decision rule

SituationChoose
Family of related classes you control, need runtime enforcementABC
Need shared (mixin) implementation alongside the contractABC
Want to type-hint a structural capability across unrelated typesProtocol
Need to accept third-party classes without modifying themProtocol
isinstance() checks at runtime are requiredABC (or @runtime_checkable Protocol)
Modern library/API code with type checkingProtocol

Recap

ABCs use nominal subtyping — classes must explicitly inherit from the ABC and implement every abstract method, which Python enforces at instantiation time. They support shared mixin implementations and integrate cleanly with isinstance. Protocols use structural subtyping — any class with the right methods and attributes satisfies the protocol without declaring it, which type checkers (mypy, pyright) verify statically. Add @runtime_checkable to get isinstance support, though only method presence is checked. Default to a Protocol for library code and capability-based interfaces; reach for an ABC when you need a tight class hierarchy, shared concrete methods, or reliable runtime enforcement.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel