Duck Typing and Protocols
Python's structural typing vs Java's nominal typing — duck typing, ABC, Protocol
Introduction
In this lesson, you'll learn about duck typing and protocols in Python. Coming from Java, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Java, you're familiar with python's structural typing vs java's nominal typing — duck typing, abc, protocol.
Python has its own approach to python's structural typing vs java's nominal typing — duck typing, abc, protocol, which we'll explore step by step.
The Python Way
Let's see how Python handles this concept. Here's a typical example:
from typing import Protocol, runtime_checkable
# Duck typing — no declaration needed
class Circle:
def draw(self) -> None: print("Circle")
def resize(self, factor: float) -> None: pass
class Square:
def draw(self) -> None: print("Square")
def resize(self, factor: float) -> None: pass
# Both work without declaring any interface
def render(shape) -> None:
shape.draw() # works if it has .draw()
render(Circle())
render(Square())
render("hello") # would fail at runtime: 'str' has no draw()
# Protocol: structural typing with static type checking
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
def render_typed(shape: Drawable) -> None:
shape.draw()
# Protocols checked by mypy/pyright at development time
isinstance(Circle(), Drawable) # True — runtime check
# ABC: nominal typing (closer to Java)
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
# Concrete classes MUST subclass ShapeComparing to Java
Here's how you might have written similar code in Java:
// Java: nominal typing — must explicitly implement
public interface Drawable {
void draw();
}
public interface Resizable {
void resize(double factor);
}
// Must explicitly declare 'implements'
public class Circle implements Drawable, Resizable {
private double radius;
@Override
public void draw() { System.out.println("Circle"); }
@Override
public void resize(double factor) { radius *= factor; }
}
// Only Circle (and declared implementors) can be used as Drawable
void render(Drawable d) { d.draw(); }
render(new Circle()); // OK
// render("string"); // Compile error — String doesn't implement DrawableYou may be used to different syntax or behavior.
Python duck typing: if it has the method, it works — no interface declaration needed
You may be used to different syntax or behavior.
Protocol (Python 3.8+) adds structural typing for static checkers without runtime obligation
You may be used to different syntax or behavior.
ABC is nominal like Java — classes must explicitly subclass
You may be used to different syntax or behavior.
isinstance(obj, Protocol) with @runtime_checkable checks structural compatibility at runtime
You may be used to different syntax or behavior.
Python's flexibility is powerful but shifts errors to runtime; Java catches at compile time
Step-by-Step Breakdown
1. Duck Typing
Python asks 'does it have the method?' not 'is it declared to have the method?'. Any object with draw() can be passed to render().
// Java: must implement Drawable
class Circle implements Drawable { void draw() {...} }# Python: just have the method
class Circle:
def draw(self): print("circle")
def render(shape): shape.draw() # works for anything with draw()2. Protocol for Static Checking
Protocol enables type checkers (mypy/pyright) to verify structural compatibility at development time — best of both worlds.
interface Drawable { void draw(); }class Drawable(Protocol):
def draw(self) -> None: ...
def render(shape: Drawable) -> None:
shape.draw()
# mypy/pyright checks that arg has draw() method3. ABC for Nominal Typing
ABC (Abstract Base Class) is Python's closest equivalent to Java interfaces — classes must subclass it and implement abstract methods.
interface Shape { double area(); }
public class Circle implements Shape { ... }from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def area(self) -> float: return 3.14 * self.r**24. Pragmatic Choice
In production Python, use Protocol for library code (flexible for users), ABC for internal hierarchies (enforces implementation).
// Java: always interfaces for APIs# Library API: Protocol (flexible, no inheritance required)
class Serializable(Protocol):
def to_json(self) -> str: ...
# Internal hierarchy: ABC (enforces completeness)
class BaseProcessor(ABC):
@abstractmethod
def process(self, data): ...Common Mistakes
When coming from Java, developers often make these mistakes:
- Python duck typing: if it has the method, it works — no interface declaration needed
- Protocol (Python 3.8+) adds structural typing for static checkers without runtime obligation
- ABC is nominal like Java — classes must explicitly subclass
Key Takeaways
- Duck typing: any object with the right methods works — no declaration needed
- Protocol: structural typing for static checkers — checked by mypy/pyright, not the runtime
- ABC: nominal typing like Java interfaces — must explicitly subclass
- Choose Protocol for flexible APIs; ABC when enforcing implementation completeness