~20 min · protocol, abc, duck-typing, structural, nominal
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
Two ways to express "has this shape"
Sometimes you want to say "this function takes anything that can be opened and closed." You don't care about the class hierarchy — you care about what methods exist. Python gives you two formal ways to express this: typing.Protocol (structural typing — match by shape) and abc.ABC (nominal typing — match by inheritance).
Duck typing — the informal version
The original Python answer to "what type does this need to be" was: don't check, just try. If it walks like a duck and quacks like a duck, it's a duck. This works for runtime behavior but tells static type checkers nothing. Modern Python keeps duck typing at runtime AND adds structural protocols at the type level.
Protocol — structural typing
Define a class inheriting from typing.Protocol with the methods/attributes you require. Any class that has those methods (with compatible signatures) is automatically considered a subtype — no inheritance needed. Static type checkers (mypy) verify this. Runtime isinstance works only if you mark the protocol @runtime_checkable, and even then it only checks method existence, not signatures.
ABC — nominal typing with abstract methods
Inherit from abc.ABC and mark methods with @abstractmethod. Subclasses must implement those methods or they can't be instantiated. ABCs enforce a contract through inheritance — you have to explicitly declare your class is a subtype. Heavier than Protocol, but appropriate when you want a real hierarchy and shared base implementation.
Principle: Use Protocol for "anything that has these methods" — flexible, no inheritance burden. Use ABC when you genuinely want a class hierarchy with shared base behavior and explicit declaration. Pippa's Adapter base in backend/adapters/base.py is an ABC because it shares helper methods and represents a real hierarchy.
Code
Duck typing — the original way·python
def total_chars(thing):
# We don't check — we just call. If thing has a way to give characters, it works.
return sum(1 for _ in thing)
print(total_chars("hello")) # 5
print(total_chars([1, 2, 3])) # 3
print(total_chars(("a", "b"))) # 2
Protocol — structural typing·python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None: ...
# Anything with a close() method is a Closable
class File:
def close(self): print("file closed")
class Connection:
def close(self): print("connection closed")
class NotClosable:
pass
def shutdown(item: Closable) -> None:
item.close()
shutdown(File()) # works — File has close
shutdown(Connection()) # works — Connection has close
# Runtime check
print(isinstance(File(), Closable)) # True
print(isinstance(NotClosable(), Closable)) # False
# Notice — File and Connection don't inherit from Closable.
# It's a structural match, not nominal.
ABC — nominal typing with abstract methods·python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
def describe(self): # concrete method, shared by all subclasses
return f"{type(self).__name__} with area {self.area()}"
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(5).describe()) # 'Square with area 25'
# Can't instantiate the abstract base
try:
Shape()
except TypeError as e:
print(e) # Can't instantiate abstract class Shape
# Subclass that DOESN'T implement area can't be instantiated either
class Broken(Shape):
pass
try:
Broken()
except TypeError as e:
print(e)
Choosing — Protocol or ABC?·python
# Use Protocol when:
# - You want flexibility (third-party classes you don't own can match)
# - There's no shared implementation, just a shape contract
from typing import Protocol
class Renderable(Protocol):
def render(self) -> str: ...
# Any class with .render() is a Renderable — including ones in libraries you can't change.
# Use ABC when:
# - You want explicit subclass declaration (Subclass MUST inherit)
# - You have shared base behavior to inherit
from abc import ABC, abstractmethod
class Brain(ABC):
@abstractmethod
async def stream(self, prompt): ...
def heal_session(self): # shared concrete method
# ... heals the session ...
pass
# In Pippa, Adapter is an ABC because all four brains share the streaming
# contract AND the healing logic, and the hierarchy is intentional.
Define a Protocol called Comparable with a method compare_to(other) -> int. Make two unrelated classes (Score(value) and WordLength(word)) both implement compare_to. Write a function sort_by_compare(items) that returns the items sorted using compare_to. Verify both class types work with the same function. Then redo the exercise as an ABC and notice the friction — both Score and WordLength would need to inherit, even though they have nothing else in common.
Progress
Progress is local-only — sign in to sync across devices.