Protocols and ABCs

Medium35 min read

Duck Typing

Why Protocols & ABCs Matters

The Problem: Duck typing is flexible but unchecked — call .quack() on the wrong thing and you find out at 3 AM.

The Solution: Abstract base classes enforce contracts at runtime; Protocol classes provide structural typing that mypy can verify without forcing inheritance.

Real Impact: Protocols are the right answer 90% of the time — they decouple your code from class hierarchies and let third-party types satisfy your interface implicitly.

Real-World Analogy

Think of Protocols as a job description and ABCs as a union card:

  • Protocol = a job description — anyone with the right skills qualifies, no membership required
  • ABC = a union card — you must explicitly join to do the job
  • Duck typing = everyone walks in unverified — works until it doesn't
  • @runtime_checkable = background check at hire time, not just at sign-up

Python's traditional approach: if it walks like a duck and quacks like a duck, it's a duck. We don't declare interfaces — any object with the right methods can be used.

def describe(thing):
    # Works on anything with .name and .area()
    print(f"{thing.name}: area = {thing.area()}")


class Circle:
    name = "Circle"
    def area(self): return 3.14

class Square:
    name = "Square"
    def area(self): return 1.0

describe(Circle())
describe(Square())

Duck typing is flexible but unchecked. Python offers two ways to formalize "behaves like X": abstract base classes and protocols.

Abstract Base Classes (ABCs)

An ABC declares which methods subclasses must implement. Instantiating an ABC directly — or a subclass that hasn't implemented all abstract methods — raises TypeError.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    # Concrete methods can call abstract ones
    def describe(self) -> str:
        return f"area={self.area()} perim={self.perimeter()}"


class Rect(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h

    def area(self) -> float:
        return self.w * self.h

    def perimeter(self) -> float:
        return 2 * (self.w + self.h)


Shape()       # TypeError — can't instantiate abstract class
Rect(3, 4)   # works

Built-in ABCs in collections.abc

Subclass these to inherit a rich set of mixin methods:

ABCYou implementYou get
Iterable__iter__for x in obj
Sized__len__len(obj)
Container__contains__x in obj
Sequence__getitem__, __len__indexing, slicing, count, index, iteration
MutableMapping5 methodsfull dict-like behavior

Protocols — Structural Typing (PEP 544)

Protocols define interfaces by structure rather than by inheritance. An object satisfies a Protocol if it has the right attributes and methods — no explicit subclassing needed. This is duck typing with type-checker support.

from typing import Protocol

class SupportsArea(Protocol):
    def area(self) -> float: ...


def total_area(shapes: list[SupportsArea]) -> float:
    return sum(s.area() for s in shapes)


# These satisfy the protocol without subclassing
class Circle:
    def area(self): return 3.14

class Rect:
    def area(self): return 1.0

total_area([Circle(), Rect()])         # type-checks, runs fine

Protocols vs ABCs

Use Protocol when consumers shouldn't need to know about your interface (typing library code, third-party types). Use ABC when you want explicit subclass enforcement at runtime.

Runtime Checks with @runtime_checkable

from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None: ...

isinstance(open("/tmp/x"), Closable)   # True

Built-in Protocols from typing

The typing module ships protocols for common patterns — use these in annotations.

ProtocolMeans
Iterable[T]Supports iter()
Iterator[T]Supports iter() + next()
Sequence[T]Indexable + sized
Mapping[K, V]Dict-like
Callable[[A, B], R]Function-like
HashableHas __hash__
SupportsInt/Float/StrCan be converted
from typing import Iterable, Mapping

def total(prices: Iterable[float]) -> float:
    return sum(prices)

def to_html(attrs: Mapping[str, str]) -> str:
    return " ".join(f'{k}="{v}"' for k, v in attrs.items())

🎯 Practice Exercises

Exercise 1: Drawable protocol

Define a Drawable Protocol with draw() -> str. Write render(items: list[Drawable]) that joins their output.

Exercise 2: Custom Sequence

Subclass collections.abc.Sequence to build a Range class. Implement __getitem__ and __len__. You get iteration, count, index, and in for free.

Exercise 3: Abstract Repository

Define an ABC Repository[T] with abstract save, find, delete. Implement a MemoryRepository and verify isinstance works.