Duck typing, and how Python handles types

What duck typing is, how ABCs and Protocol affect it, and how type hints coexist with duck typing.

“If it walks like a duck and quacks like a duck, it’s a duck.”

What is duck typing?

Duck typing is the programming philosophy of judging an object’s type by the methods and attributes it has, not by its class declaration. It is a common approach in dynamically typed languages such as Python, JavaScript, and Ruby.

Rather than checking the type explicitly, you simply call the behaviour you need. If the method or attribute exists, it works; if not, you get a runtime error.

class Duck:
    def sound(self):
        return "Quack!"

class Dog:
    def sound(self):
        return "Woof!"

class Robot:
    def sound(self):
        return "Beep!"

def make_sound(animal):
    # just call it, no type check
    print(animal.sound())

make_sound(Duck())   # Quack!
make_sound(Dog())    # Woof!
make_sound(Robot())  # Beep!

Duck, Dog, Robot — all are treated identically as long as they have a sound() method. The focus is on “what can this object do?” rather than “what is this object?”

Do ABCs break duck typing?

The short answer is not necessarily. It depends on how you use them.

Forcing an isinstance() check → duck typing broken

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def sound(self): pass

def make_sound(animal):
    if not isinstance(animal, Animal):  # checking the type directly!
        raise TypeError("not an Animal")
    print(animal.sound())

make_sound(Robot())  # ❌ rejected even though it has sound()

ABC as specification only, no check → duck typing preserved

If you do not check the type inside the function, duck typing keeps working even with an ABC in play. All the ABC does then is act as an agreement among developers: “this method must be implemented.”

A compromise with register()

Animal.register(Robot)  # register Robot as an Animal

print(isinstance(Robot(), Animal))  # ✅ True (passes without inheritance)

This is a middle ground that keeps duck typing’s flexibility while making partial use of the type system. The drawback is that a developer has to register things manually.

Situation Duck typing
ABC + forced isinstance() check ❌ broken
ABC defined only, called without a check ✅ preserved
ABC + register() 〰 compromise

Type hints have nothing to do with duck typing

Type hints are completely ignored at runtime. Writing animal: Duck does not make the Python interpreter check anything during execution.

def make_sound(animal: Duck) -> str:
    return animal.sound()

make_sound(Robot())  # ✅ runs without any problem

What type hints actually do is this:

  • Static analysis: type checkers like mypy and pyright warn you before the code runs
  • IDE support: used for autocompletion and warnings
  • Documentation: conveying intent to whoever reads the code

Wiring mypy into CI effectively constrains duck typing, but that is a check at development time. The runtime behaviour is still duck typing.

Protocol: duck typing for type hints

When you want to use mypy and still keep the spirit of duck typing, you use Protocol. The whole reason Protocol exists is type hints and static analysis.

from typing import Protocol

class Soundable(Protocol):
    def sound(self) -> str: ...

def make_sound(animal: Soundable) -> str:
    # "having a sound() method is enough"
    return animal.sound()

class Robot:  # without inheriting from Soundable
    def sound(self):
        return "Beep!"

make_sound(Robot())  # ✅ passes mypy, passes at runtime

This is called structural subtyping. It treats an object as belonging to a type if it has the required methods, with no inheritance relationship — the spirit of duck typing, implemented inside the world of type hints.

@runtime_checkable: using it at runtime too

from typing import Protocol, runtime_checkable

@runtime_checkable
class Soundable(Protocol):
    def sound(self) -> str: ...

print(isinstance(Robot(), Soundable))  # ✅ True
# checks for the presence of the methods automatically

This resembles an ABC’s register(), but there is no manual registration: the decision is made automatically from the presence of the methods.

Approach Duck typing Static analysis Runtime isinstance
Pure duck typing
ABC + register() ✅ (manual)
Protocol
Protocol + runtime_checkable ✅ (automatic)

Summary

  • Duck typing judges a type by “what it can do”, not “what it is”.
  • Depending on how you use them, ABCs can either break duck typing or preserve it.
  • Type hints are ignored at runtime, so they do not directly affect duck typing.
  • Protocol is the way to keep the duck typing philosophy in a type-hinted, statically analysed codebase.