Back to Home
Live Project · Protocol Checker

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.

📅 October 20, 2026 ⏱ 18 min read 🎓 Intermediate 🐍 Python 3.8+
Back to Home Try the Checker See the Code Polymorphism Guide

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.

    Live protocol checker Runnable Python code 6 built-in protocols explained Playground + quiz

    Two Ways to Say "This Has a Method"

    ApproachHow you define itChecked byChecked when
    Plain duck typingJust call the methodNothingAt 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 creationWhen 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

    __iter__

    Can be looped over with for.

    Sized

    __len__

    Has a length — len() works.

    Comparable

    __lt__

    Can be sorted or compared with <.

    Hashable

    __hash__

    Can be a set element or dict key.

    Callable

    __call__

    Can be called like a function: obj().

    ContextManager

    __enter__
    __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.

    ProtocolRequiresExample
    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.

    📊 Counter 📋 SortedList 🌡️ Temperature 📍 Point 🔔 Callback 📄 FakeFile
    Press Check Protocols to run.

    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))
    Expected output
    Duck satisfies Speaker: True Dog satisfies Speaker: True Rock satisfies Speaker: False BrokenDuck satisfies Speaker: False ---------------------------------------- Bag is Iterable : True Bag is Sized : True Bag is Hashable : True Bag is Callable : False True True True True False

    🔍 Deep dive — what each check actually does

    ObjectProtocolResultWhy
    Duck()Speaker✅ TrueHas a speak method.
    Dog()Speaker✅ TrueAlso has speak. Different behavior, same contract.
    Rock()Speaker❌ FalseNo speak attribute at all.
    BrokenDuck()Speaker❌ Falsespeak 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✅ TrueDefines __iter__.
    Bag(1,2,3)Sized✅ TrueDefines __len__.
    Bag(1,2,3)Hashable✅ TrueDefault __hash__ from object (since __eq__ isn't overridden).
    Bag(1,2,3)Callable❌ FalseNo __call__ method.
    3.14Iterable❌ FalseFloats 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

    1. @runtime_checkable + class Speaker(Protocol) — declares a protocol. Without @runtime_checkable, isinstance would raise TypeError; only static type checking would work.
    2. def speak(self) -> str: ... — the required shape. The body is just ... because it's never called; it exists only to declare the contract.
    3. Duck, Dog, Rock — three classes with no inheritance relationship. Two have speak(); one doesn't.
    4. BrokenDuck with speak = "Quack" — speak is a plain string attribute, not a method. In modern Python the runtime check rejects it.
    5. The first loop — isinstance(obj, Speaker) returns True only for objects with a proper speak attribute. Notice there's no explicit inheritance anywhere.
    6. 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.
    7. class Bag — a plain class that defines __iter__ and __len__. It doesn't inherit from Iterable, but it still satisfies the protocol by structural typing.
    8. The second loop — isinstance(bag, proto) for four protocols. Iterable and Sized pass because the methods exist.
    9. The last three prints — demonstrate protocols on built-in objects: lists, strings, ints, lambdas, floats.
    10. 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_checkable from Speaker and re-run the code. What error appears?
    • Add a Cat class with speak() and confirm it also passes. How many lines of existing code did you edit?
    • Make Bag define __call__ and confirm isinstance(bag, Callable) flips to True.
    • Define your own Writable protocol requiring write(text) and check it against io.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.

    🐍 protocols.py
    Press Run to execute.

    Quick Knowledge Check

    Five questions on Protocols and structural typing.

    0/5

    Great effort — review any missed questions above.

    Share This Project

    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.

    Weekly Python tutorials OOP series updates Free, cancel anytime

    🔒 Your email is never shared or sold.