Protocol Checker —
Does It Quack?
Check It Live.
Duck typing says: "If it has the methods I call, it's good enough." But how do you verify that without writing a test for every class? Enter typing.Protocol with @runtime_checkable — Python's built-in answer.
Paste any class into the live checker below and see which protocols it satisfies — Iterable, Sized, Comparable, Hashable, Callable and more. No inheritance required, just structural typing in action.
Progress Tracker
Tick off each milestone as you complete it. Progress is saved in this browser.
🎯 Your Journey Through Protocols
0%Structural Typing, Written Down
🦆 Duck Typing, But Explicit
Python's duck typing says: "If it walks like a duck and quacks like a duck, it's a duck." You don't need a shared base class — just matching methods.
But duck typing has a problem: it fails late. You find out a class is missing a method only when the method is called at runtime. A typing.Protocol lets you write the duck contract down, and with @runtime_checkable you can even check it with isinstance.
This is structural typing: "does this object have the shape I need?" — checked at runtime or by a type checker, without requiring inheritance.
Two Ways to Say "This Has a Method"
| Approach | How you define it | Checked by | Checked when |
|---|---|---|---|
| Plain duck typing | Just call the method | Nothing | At call time (fails late) |
| Protocol (this page) | class Speaker(Protocol): ... | isinstance (runtime) + mypy (static) | At the isinstance call, or by mypy |
| Inheritance (ABC) | class Speaker(ABC): ... | isinstance + class creation | When the class is instantiated |
The key insight: a Protocol is a duck-typing contract that you can check. It doesn't force inheritance, but it lets you ask isinstance(obj, Speaker) and get True only if the object has the right methods.
Meet the Protocols
Each protocol is a class that declares one or more required methods or attributes. Anything that "has" them satisfies the protocol.
Iterable
Can be looped over with for.
Sized
Has a length — len() works.
Comparable
Can be sorted or compared with <.
Hashable
Can be a set element or dict key.
Callable
Can be called like a function: obj().
ContextManager
__exit__
Can be used in with blocks.
Protocols in the Standard Library
typing ships several ready-made protocols. You never need to define these yourself — just import and check against them.
| Protocol | Requires | Example |
|---|---|---|
Iterable[T] | __iter__ | isinstance([1,2], Iterable) |
Sized | __len__ | isinstance("abc", Sized) |
Hashable | __hash__ | isinstance(42, Hashable) |
Callable[[], int] | __call__ | isinstance(lambda: 1, Callable) |
ContextManager[T] | __enter__, __exit__ | isinstance(open("f"), ContextManager) |
Runtime caveat: for a bare protocol like Iterable, isinstance(x, Iterable) only checks that __iter__ exists — not that it works correctly. It's a name check, not a functional test.
Check Any Class Against Protocols
Paste a Python class definition (or use one of the examples) and press Check Protocols. The checker runs the class through isinstance against six built-in protocols and reports which pass.
What's happening under the hood: the checker imports typing built-in protocols, evaluates your class in a sandboxed namespace, instantiates it, and calls isinstance(instance, Protocol) for each one. No inheritance needed.
The Python Implementation
Here's the complete code that powers the checker. Read it once, then press Run to execute it in your browser.
A Custom Protocol and a Checker
A custom Speaker protocol, three classes — two that satisfy it, one that doesn't — and a loop that checks each with isinstance.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Speaker(Protocol):
def speak(self) -> str:
...
class Duck:
def speak(self):
return "Quack"
class Dog:
def speak(self):
return "Woof"
class Rock:
pass
class BrokenDuck:
speak = "Quack"
for obj in (Duck(), Dog(), Rock(), BrokenDuck()):
name = type(obj).__name__
ok = isinstance(obj, Speaker)
print(f"{name:12s} satisfies Speaker: {ok}")
print("-" * 40)
from collections.abc import Iterable, Sized, Callable, Hashable
class Bag:
def __init__(self, *items):
self._items = list(items)
def __iter__(self):
return iter(self._items)
def __len__(self):
return len(self._items)
bag = Bag(1, 2, 3)
for proto in (Iterable, Sized, Hashable, Callable):
name = proto.__name__
print(f"Bag is {name:12s}: {isinstance(bag, proto)}")
print(isinstance([1, 2], Iterable), isinstance("hi", Sized))
print(isinstance(42, Hashable), isinstance(lambda: 1, Callable))
print(isinstance(3.14, Iterable))🔍 Deep dive — what each check actually does
| Object | Protocol | Result | Why |
|---|---|---|---|
Duck() | Speaker | ✅ True | Has a speak method. |
Dog() | Speaker | ✅ True | Also has speak. Different behavior, same contract. |
Rock() | Speaker | ❌ False | No speak attribute at all. |
BrokenDuck() | Speaker | ❌ False | speak is a string, not a callable — the runtime check looks for the name and that it's not a plain data attribute. |
Bag(1,2,3) | Iterable | ✅ True | Defines __iter__. |
Bag(1,2,3) | Sized | ✅ True | Defines __len__. |
Bag(1,2,3) | Hashable | ✅ True | Default __hash__ from object (since __eq__ isn't overridden). |
Bag(1,2,3) | Callable | ❌ False | No __call__ method. |
3.14 | Iterable | ❌ False | Floats aren't iterable. |
Key insight: isinstance with a runtime_checkable Protocol only checks name presence — it does not verify signatures or that methods actually work. That's the static type checker's job (mypy, pyright).
📖 Line-by-line walkthrough
@runtime_checkable+class Speaker(Protocol)— declares a protocol. Without@runtime_checkable,isinstancewould raiseTypeError; only static type checking would work.def speak(self) -> str: ...— the required shape. The body is just...because it's never called; it exists only to declare the contract.Duck,Dog,Rock— three classes with no inheritance relationship. Two havespeak(); one doesn't.BrokenDuckwithspeak = "Quack"—speakis a plain string attribute, not a method. In modern Python the runtime check rejects it.- The first loop —
isinstance(obj, Speaker)returnsTrueonly for objects with a properspeakattribute. Notice there's no explicit inheritance anywhere. from collections.abc import Iterable, Sized, Callable, Hashable— Python's built-in protocols. These are the same ones used to describe the standard library's own types.class Bag— a plain class that defines__iter__and__len__. It doesn't inherit fromIterable, but it still satisfies the protocol by structural typing.- The second loop —
isinstance(bag, proto)for four protocols. Iterable and Sized pass because the methods exist. - The last three prints — demonstrate protocols on built-in objects: lists, strings, ints, lambdas, floats.
isinstance(3.14, Iterable)→False— floats aren't iterable. The check works on any object, not just your own classes.
⚠️ Common mistakes
Forgetting @runtime_checkable. Without it, isinstance(obj, MyProtocol) raises TypeError: Instance and class checks can only be used with @runtime_checkable protocols.
Expecting signature checks. @runtime_checkable only checks name existence. A method with the wrong arguments still passes the runtime check — mypy is the one that catches signature mismatches.
Checking non-method attributes. Runtime protocols in older Pythons didn't support data members (attributes) — only methods. Even now, treat protocol data members carefully.
Using protocols as base classes. You can inherit from a Protocol, but you don't need to. The whole point is that you don't have to touch the class you're checking.
🧪 Try it yourself
Experiments in the playground below:
- Remove
@runtime_checkablefromSpeakerand re-run the code. What error appears? - Add a
Catclass withspeak()and confirm it also passes. How many lines of existing code did you edit? - Make
Bagdefine__call__and confirmisinstance(bag, Callable)flips toTrue. - Define your own
Writableprotocol requiringwrite(text)and check it againstio.StringIO.
Experiment With Protocols
Define your own protocol, test it against built-in and custom classes, and see isinstance work on structural typing. Press Run to execute.
Quick Knowledge Check
Five questions on Protocols and structural typing.
Great effort — review any missed questions above.
Explore the Full OOP Series
This project is one of 10 live apps in the Polymorphism guide. Each project shows a different flavor of polymorphism in a working app.
Each shape class overrides draw().
Any class with apply() plugs in.
Money arithmetic with dunder methods.
OperatorsPer-type quantity formatting.
SingledispatchSongs, podcasts and videos share a play loop.
OverridingFake files and real files look the same.
Duck TypingDebits and credits support + and __neg__.
Formats logs per type.
SingledispatchEach piece overrides legal_moves().
Live isinstance with Protocols.
Explore the Full OOP Series
Subscribe to Poetic Bytes
Level up your Python knowledge.
In-depth Python tutorials, AI insights, and software engineering guides — delivered straight to your inbox. No spam, ever.
🔒 Your email is never shared or sold.