Gyaan

Duck Typing and Protocols

intermediate duck-typing protocols structural-typing EAFP

“If it walks like a duck and quacks like a duck, then it must be a duck.” This is the core philosophy behind Python’s type system. We don’t care what an object is — we care what it can do.

What Is Duck Typing?

In languages like Java, we need to explicitly declare that a class implements an interface. In Python, we just use the object. If it has the method we need, it works.

class Duck:
    def quack(self):
        print("Quack!")

class Person:
    def quack(self):
        print("I'm quacking like a duck!")

def make_it_quack(thing):
    thing.quack()  # we don't check the type — just call the method

make_it_quack(Duck())    # Quack!
make_it_quack(Person())  # I'm quacking like a duck!

make_it_quack doesn’t ask “are you a Duck?” — it just asks “can you quack?” That’s duck typing.

EAFP vs LBYL

Python’s duck typing culture leads to a coding style called EAFP — “Easier to Ask Forgiveness than Permission.” Instead of checking if something is possible before doing it, we just try and handle the failure.

# LBYL (Look Before You Leap) — non-Pythonic
if hasattr(obj, "quack"):
    obj.quack()

# EAFP (Easier to Ask Forgiveness) — Pythonic
try:
    obj.quack()
except AttributeError:
    print("This object can't quack")

EAFP is preferred because it’s faster in the common case (no extra check) and avoids race conditions.

Python’s Built-in Protocols

Python already uses duck typing everywhere through implicit “protocols.” If our object has the right methods, it works with built-in features:

  • Iterable — has __iter__() → works with for loops
  • Callable — has __call__() → can be called like a function
  • Context Manager — has __enter__() and __exit__() → works with with
  • Subscriptable — has __getitem__() → supports obj[key]
  • Comparable — has __eq__(), __lt__(), etc. → works with ==, <
  • Hashable — has __hash__() → can be used as dict key or in sets
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

# Works with for loops — because it has __iter__ and __next__
for num in Countdown(3):
    print(num)  # 3, 2, 1

We didn’t inherit from any base class. We just implemented the right methods, and Python’s for loop works with it.

typing.Protocol: Explicit Structural Typing

Since Python 3.8, the typing module gives us Protocol — a way to formally define what methods an object should have, without requiring inheritance.

from typing import Protocol

class Renderable(Protocol):
    def render(self) -> str: ...

class HTMLWidget:
    def render(self) -> str:
        return "<div>Widget</div>"

class JSONData:
    def render(self) -> str:
        return '{"key": "value"}'

def display(item: Renderable) -> None:
    print(item.render())

display(HTMLWidget())  # works — has render()
display(JSONData())    # works — has render()

HTMLWidget and JSONData never mention Renderable. They just happen to have a render() method. The type checker sees the match and approves.

runtime_checkable

By default, Protocol only works for static type checking (mypy). If we want isinstance() checks at runtime, we add @runtime_checkable:

from typing import Protocol, runtime_checkable

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

import io
f = io.StringIO()
print(isinstance(f, Closeable))  # True — StringIO has close()
print(isinstance(42, Closeable)) # False — int doesn't have close()

Note: runtime_checkable only checks if the methods exist, not their signatures. It’s a quick duck-type check, not a full type validation.

Protocol vs ABC (Abstract Base Classes)

Both define interfaces, but they work differently:

  • ABC — requires explicit inheritance (class MyClass(MyABC)). It’s nominal typing — “I declare that I implement this.”
  • Protocol — no inheritance needed. It’s structural typing — “I have the right methods, so I match.”
from abc import ABC, abstractmethod

# ABC approach — must inherit
class Drawable(ABC):
    @abstractmethod
    def draw(self): ...

class Circle(Drawable):  # must explicitly inherit
    def draw(self):
        print("Drawing circle")

# Protocol approach — no inheritance
from typing import Protocol

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

class Square:  # no inheritance needed
    def draw(self):
        print("Drawing square")

Use ABCs when we own the class hierarchy and want to enforce a contract. Use Protocols when we want to accept any object that has the right shape — especially useful for third-party code we can’t modify.

In simple language, duck typing is Python saying “show me what you can do, not who you are.” Protocols take this idea and give it structure — we can describe the shape we need without forcing anyone to inherit from our classes.