OOP Intermediate
Protocols
Structural Typing
You want a function that accepts "anything with a read() method". With ABC, the
object must inherit from your base class. Protocols check structure instead -
if it has read(), it's compatible. Static type checking meets duck typing.
Basic protocol
Define an interface by structure.
# Basic Protocols
from typing import Protocol, runtime_checkable
# Define a protocol
@runtime_checkable
class Drawable(Protocol):
"""
Protocol for drawable objects.
Any class with draw() method is compatible.
"""
def draw(self) -> str:
"""Draw the object and return description."""
...
# Classes that satisfy the protocol
# Note: NO inheritance from Drawable!
class Circle:
"""Circle class - has draw() method."""
def __init__(self, radius):
self.radius = radius
def draw(self) -> str:
return f"Drawing circle with radius {self.radius}"
class Rectangle:
"""Rectangle class - has draw() method."""
def __init__(self, width, height):
self.width = width
self.height = height
def draw(self) -> str:
return f"Drawing rectangle {self.width}x{self.height}"
class Text:
"""Text class - has draw() method."""
def __init__(self, content):
self.content = content
def draw(self) -> str:
return f"Drawing text: '{self.content}'"
class Line:
"""Line class - has draw() method."""
def __init__(self, start, end):
self.start = start
self.end = end
def draw(self) -> str:
return f"Drawing line from {self.start} to {self.end}"
# Function using the protocol as type hint
def render_shape(shape: Drawable) -> None:
"""
Render any Drawable object.
Works with Circle, Rectangle, Text, Line - anything with draw().
"""
print(f"Rendering: {shape.draw()}")
def render_all(shapes: list[Drawable]) -> None:
"""Render multiple drawable objects."""
print("=== Rendering All Shapes ===")
for shape in shapes:
render_shape(shape)
print("=== Basic Protocols ===\n")
# Create objects (none inherit from Drawable!)
circle = Circle(5)
rectangle = Rectangle(10, 20)
text = Text("Hello")
line = Line((0, 0), (10, 10))
# All work with render_shape()
print("--- Individual Rendering ---")
render_shape(circle)
render_shape(rectangle)
render_shape(text)
render_shape(line)
print()
# Render all together
shapes =
render_all(shapes)
# Object without draw() method
print("\n--- Non-Drawable Object ---")
class Point:
"""Point has no draw() method."""
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(5, 10)
# This would cause a type error (if using type checker)
# but still runs because Python is dynamically typed
print("Point object created (no draw method)")
print("Type checkers would flag: render_shape(point)")
# Demonstrate that it's structural, not nominal
print("\n--- Structural Typing Proof ---")
print(f"Circle inherits from Drawable: {issubclass(Circle, Drawable) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")
print("\n=== Protocol Key Points ===")
print("""
1. Define interface with Protocol:
class MyProtocol(Protocol):
def method(self) -> Type: ...
2. No inheritance required:
- Classes just need matching methods
- "Structural subtyping"
3. Works with type checkers:
- mypy, pyright, etc.
- Catches mismatches at development time
4. ... (ellipsis) vs pass:
- Both work in protocols
- ... is convention for "to be implemented"
5. Duck typing formalized:
- "If it walks like a duck..."
- Now with type hints!
""")
# Basic Protocols
from typing import Protocol, runtime_checkable
# Define a protocol
@runtime_checkable
class Drawable(Protocol):
"""
Protocol for drawable objects.
Any class with draw() method is compatible.
"""
def draw(self) -> str:
"""Draw the object and return description."""
...
# Classes that satisfy the protocol
# Note: NO inheritance from Drawable!
class Circle:
"""Circle class - has draw() method."""
def __init__(self, radius):
self.radius = radius
def draw(self) -> str:
return f"Drawing circle with radius {self.radius}"
class Rectangle:
"""Rectangle class - has draw() method."""
def __init__(self, width, height):
self.width = width
self.height = height
def draw(self) -> str:
return f"Drawing rectangle {self.width}x{self.height}"
class Text:
"""Text class - has draw() method."""
def __init__(self, content):
self.content = content
def draw(self) -> str:
return f"Drawing text: '{self.content}'"
class Line:
"""Line class - has draw() method."""
def __init__(self, start, end):
self.start = start
self.end = end
def draw(self) -> str:
return f"Drawing line from {self.start} to {self.end}"
# Function using the protocol as type hint
def render_shape(shape: Drawable) -> None:
"""
Render any Drawable object.
Works with Circle, Rectangle, Text, Line - anything with draw().
"""
print(f"Rendering: {shape.draw()}")
def render_all(shapes: list[Drawable]) -> None:
"""Render multiple drawable objects."""
print("=== Rendering All Shapes ===")
for shape in shapes:
render_shape(shape)
print("=== Basic Protocols ===\n")
# Create objects (none inherit from Drawable!)
circle = Circle(5)
rectangle = Rectangle(10, 20)
text = Text("Hello")
line = Line((0, 0), (10, 10))
# All work with render_shape()
print("--- Individual Rendering ---")
render_shape(circle)
render_shape(rectangle)
render_shape(text)
render_shape(line)
print()
# Render all together
shapes =
render_all(shapes)
# Object without draw() method
print("\n--- Non-Drawable Object ---")
class Point:
"""Point has no draw() method."""
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(5, 10)
# This would cause a type error (if using type checker)
# but still runs because Python is dynamically typed
print("Point object created (no draw method)")
print("Type checkers would flag: render_shape(point)")
# Demonstrate that it's structural, not nominal
print("\n--- Structural Typing Proof ---")
print(f"Circle inherits from Drawable: {issubclass(Circle, Drawable) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")
print("\n=== Protocol Key Points ===")
print("""
1. Define interface with Protocol:
class MyProtocol(Protocol):
def method(self) -> Type: ...
2. No inheritance required:
- Classes just need matching methods
- "Structural subtyping"
3. Works with type checkers:
- mypy, pyright, etc.
- Catches mismatches at development time
4. ... (ellipsis) vs pass:
- Both work in protocols
- ... is convention for "to be implemented"
5. Duck typing formalized:
- "If it walks like a duck..."
- Now with type hints!
""")
# Basic Protocols
from typing import Protocol, runtime_checkable
# Define a protocol
@runtime_checkable
class Drawable(Protocol):
"""
Protocol for drawable objects.
Any class with draw() method is compatible.
"""
def draw(self) -> str:
"""Draw the object and return description."""
...
# Classes that satisfy the protocol
# Note: NO inheritance from Drawable!
class Circle:
"""Circle class - has draw() method."""
def __init__(self, radius):
self.radius = radius
def draw(self) -> str:
return f"Drawing circle with radius {self.radius}"
class Rectangle:
"""Rectangle class - has draw() method."""
def __init__(self, width, height):
self.width = width
self.height = height
def draw(self) -> str:
return f"Drawing rectangle {self.width}x{self.height}"
class Text:
"""Text class - has draw() method."""
def __init__(self, content):
self.content = content
def draw(self) -> str:
return f"Drawing text: '{self.content}'"
class Line:
"""Line class - has draw() method."""
def __init__(self, start, end):
self.start = start
self.end = end
def draw(self) -> str:
return f"Drawing line from {self.start} to {self.end}"
# Function using the protocol as type hint
def render_shape(shape: Drawable) -> None:
"""
Render any Drawable object.
Works with Circle, Rectangle, Text, Line - anything with draw().
"""
print(f"Rendering: {shape.draw()}")
def render_all(shapes: list[Drawable]) -> None:
"""Render multiple drawable objects."""
print("=== Rendering All Shapes ===")
for shape in shapes:
render_shape(shape)
print("=== Basic Protocols ===\n")
# Create objects (none inherit from Drawable!)
circle = Circle(5)
rectangle = Rectangle(10, 20)
text = Text("Hello")
line = Line((0, 0), (10, 10))
# All work with render_shape()
print("--- Individual Rendering ---")
render_shape(circle)
render_shape(rectangle)
render_shape(text)
render_shape(line)
print()
# Render all together
shapes =
render_all(shapes)
# Object without draw() method
print("\n--- Non-Drawable Object ---")
class Point:
"""Point has no draw() method."""
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(5, 10)
# This would cause a type error (if using type checker)
# but still runs because Python is dynamically typed
print("Point object created (no draw method)")
print("Type checkers would flag: render_shape(point)")
# Demonstrate that it's structural, not nominal
print("\n--- Structural Typing Proof ---")
print(f"Circle inherits from Drawable: {issubclass(Circle, Drawable) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")
print("\n=== Protocol Key Points ===")
print("""
1. Define interface with Protocol:
class MyProtocol(Protocol):
def method(self) -> Type: ...
2. No inheritance required:
- Classes just need matching methods
- "Structural subtyping"
3. Works with type checkers:
- mypy, pyright, etc.
- Catches mismatches at development time
4. ... (ellipsis) vs pass:
- Both work in protocols
- ... is convention for "to be implemented"
5. Duck typing formalized:
- "If it walks like a duck..."
- Now with type hints!
""")
from typing import Protocol. Define methods the type must have.
Protocol with attributes
Protocols can require attributes too.
# Protocols with Attributes and Methods
from typing import Protocol
# Protocol with both methods and attributes
class Vehicle(Protocol):
"""
Protocol for vehicles.
Requires specific attributes AND methods.
"""
# Attribute declarations
brand: str
model: str
year: int
# Method declarations
def start(self) -> str:
"""Start the vehicle."""
...
def stop(self) -> str:
"""Stop the vehicle."""
...
def get_info(self) -> str:
"""Get vehicle info."""
...
# Classes implementing the protocol
class Car:
"""Car satisfies Vehicle protocol."""
def __init__(self, brand: str, model: str, year: int):
self.brand = brand # Required attribute
self.model = model # Required attribute
self.year = year # Required attribute
self._running = False
def start(self) -> str:
self._running = True
return f"{self.brand} {self.model} engine started"
def stop(self) -> str:
self._running = False
return f"{self.brand} {self.model} engine stopped"
def get_info(self) -> str:
status = "running" if self._running else "stopped"
return f"{self.year} {self.brand} {self.model} ({status})"
class Motorcycle:
"""Motorcycle satisfies Vehicle protocol."""
def __init__(self, brand: str, model: str, year: int):
self.brand = brand
self.model = model
self.year = year
self._running = False
def start(self) -> str:
self._running = True
return f"{self.brand} {self.model} roars to life!"
def stop(self) -> str:
self._running = False
return f"{self.brand} {self.model} goes silent"
def get_info(self) -> str:
status = "running" if self._running else "parked"
return f"{self.year} {self.brand} {self.model} ({status})"
class ElectricScooter:
"""Electric scooter - also satisfies Vehicle protocol."""
def __init__(self, brand: str, model: str, year: int):
self.brand = brand
self.model = model
self.year = year
self._powered = False
self.battery_level = 100
def start(self) -> str:
self._powered = True
return f"{self.brand} {self.model} silently powers on"
def stop(self) -> str:
self._powered = False
return f"{self.brand} {self.model} powers off"
def get_info(self) -> str:
status = "on" if self._powered else "off"
return f"{self.year} {self.brand} {self.model} ({status}, {self.battery_level}% battery)"
# Functions using Vehicle protocol
def test_drive(vehicle: Vehicle) -> None:
"""Test drive any vehicle."""
print(f"Testing: {vehicle.get_info()}")
print(f" → {vehicle.start()}")
print(f" → {vehicle.stop()}")
def vehicle_summary(vehicles: list[Vehicle]) -> None:
"""Print summary of all vehicles."""
print("\n=== Vehicle Fleet ===")
for v in vehicles:
print(f" {v.year} {v.brand} {v.model}")
def find_by_brand(vehicles: list[Vehicle], brand: str) -> list[Vehicle]:
"""Find vehicles by brand."""
return [v for v in vehicles if v.brand.lower() == brand.lower()]
print("=== Protocols with Attributes ===\n")
# Create vehicles
car = Car("Toyota", "Camry", 2022)
motorcycle = Motorcycle("Harley-Davidson", "Street 750", 2021)
scooter = ElectricScooter("Xiaomi", "Mi Electric", 2023)
vehicles = [car, motorcycle, scooter]
# Test drive each
print("--- Test Drives ---")
for v in vehicles:
test_drive(v)
print()
# Summary - accessing protocol attributes
vehicle_summary(vehicles)
# Search by brand
print("\n--- Search by Brand ---")
toyotas = find_by_brand(vehicles, "toyota")
print(f"Toyota vehicles: {[f'{v.model}' for v in toyotas]}")
# Incomplete implementation
print("\n--- Incomplete Implementation ---")
class Bicycle:
"""Bicycle is missing some protocol requirements."""
def __init__(self, brand):
self.brand = brand
# Missing: model, year attributes
def start(self) -> str:
return "Start pedaling"
# Missing: stop(), get_info() methods
bicycle = Bicycle("Trek")
print(f"Bicycle created: {bicycle.brand}")
print("Type checker would flag Bicycle as not satisfying Vehicle protocol")
print("Missing: model, year, stop(), get_info()")
print("\n=== Protocol Attribute Rules ===")
print("""
1. Attribute declarations in Protocol:
class MyProtocol(Protocol):
name: str # Required attribute
value: int # Required attribute
2. Implementing classes need:
- Same attribute names
- Same types (for type checkers)
3. Attributes can have default values in implementations
4. Protocol just checks structure:
- Has the attribute? ✓
- Has the method? ✓
- Correct types? ✓ (type checker)
""")
Define attributes as class variables with types. Implementers must have them.
Runtime checkable
Make protocols work with isinstance().
# Runtime Checkable Protocols
from typing import Protocol, runtime_checkable
# Regular protocol (NOT runtime checkable)
class Speakable(Protocol):
"""Protocol without @runtime_checkable."""
def speak(self) -> str:
...
# Runtime checkable protocol
@runtime_checkable
class Walkable(Protocol):
"""
Protocol with @runtime_checkable.
Can use isinstance() with this protocol.
"""
def walk(self) -> str:
...
@runtime_checkable
class Swimmable(Protocol):
"""Another runtime checkable protocol."""
def swim(self) -> str:
...
# Classes implementing protocols
class Dog:
"""Dog can walk and speak."""
def walk(self) -> str:
return "Dog walks on four legs"
def speak(self) -> str:
return "Woof!"
class Fish:
"""Fish can only swim."""
def swim(self) -> str:
return "Fish swims with fins"
class Duck:
"""Duck can do everything!"""
def walk(self) -> str:
return "Duck waddles"
def swim(self) -> str:
return "Duck paddles on water"
def speak(self) -> str:
return "Quack!"
class Robot:
"""Robot can walk."""
def walk(self) -> str:
return "Robot walks mechanically"
print("=== Runtime Checkable Protocols ===\n")
# Create objects
dog = Dog()
fish = Fish()
duck = Duck()
robot = Robot()
objects = [dog, fish, duck, robot]
# Test with runtime checkable protocol
print("--- isinstance() with @runtime_checkable ---")
print("\nWalkable check (has walk() method?):")
for obj in objects:
name = type(obj).__name__
is_walkable = isinstance(obj, Walkable)
print(f" {name}: isinstance(obj, Walkable) = {is_walkable}")
print("\nSwimmable check (has swim() method?):")
for obj in objects:
name = type(obj).__name__
is_swimmable = isinstance(obj, Swimmable)
print(f" {name}: isinstance(obj, Swimmable) = {is_swimmable}")
# Test with non-runtime-checkable protocol
print("\n--- isinstance() without @runtime_checkable ---")
try:
result = isinstance(dog, Speakable)
print(f"Dog isinstance(Speakable) = {result}")
except TypeError as e:
print(f"Error: {e}")
print("Cannot use isinstance() with non-runtime-checkable protocol!")
# Practical use: filtering by capability
print("\n--- Filtering by Capability ---")
def get_walkers(items: list) -> list[Walkable]:
"""Filter items that can walk."""
return [item for item in items if isinstance(item, Walkable)]
def get_swimmers(items: list) -> list[Swimmable]:
"""Filter items that can swim."""
return [item for item in items if isinstance(item, Swimmable)]
walkers = get_walkers(objects)
print(f"Walkers: {[type(w).__name__ for w in walkers]}")
swimmers = get_swimmers(objects)
print(f"Swimmers: {[type(s).__name__ for s in swimmers]}")
# Process only matching objects
print("\n--- Process Only Walkers ---")
for obj in objects:
if isinstance(obj, Walkable):
print(f" {type(obj).__name__}: {obj.walk()}")
print("\n--- Process Only Swimmers ---")
for obj in objects:
if isinstance(obj, Swimmable):
print(f" {type(obj).__name__}: {obj.swim()}")
# Combining protocols
print("\n--- Finding Multi-talented (can both walk AND swim) ---")
for obj in objects:
if isinstance(obj, Walkable) and isinstance(obj, Swimmable):
name = type(obj).__name__
print(f" {name} can:")
print(f" - {obj.walk()}")
print(f" - {obj.swim()}")
print("\n=== @runtime_checkable Rules ===")
print("""
1. Without @runtime_checkable:
- Protocol is for static type checking only
- Cannot use isinstance()
2. With @runtime_checkable:
- Can use isinstance() at runtime
- Checks if object has the required methods
- Does NOT check method signatures
3. Limitations of runtime checking:
- Only checks method/attribute names exist
- Does NOT verify return types
- Does NOT verify parameter types
- Less strict than static type checking
4. When to use:
- Need to filter objects by capability
- Conditional logic based on protocol
- Building plugin systems
""")
@runtime_checkable decorator enables isinstance() checks.
Protocol vs ABC
When to use each approach.
# Protocol vs ABC Comparison
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable
print("=== Protocol vs ABC ===\n")
# ========== ABC APPROACH ==========
print("--- ABC (Abstract Base Class) ---")
class ShapeABC(ABC):
"""
Abstract Base Class approach.
Subclasses MUST inherit from this.
"""
@abstractmethod
def area(self) -> float:
"""Calculate area."""
pass
@abstractmethod
def perimeter(self) -> float:
"""Calculate perimeter."""
pass
class CircleABC(ShapeABC):
"""Circle MUST inherit from ShapeABC."""
PI = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return CircleABC.PI * self.radius ** 2
def perimeter(self) -> float:
return 2 * CircleABC.PI * self.radius
class RectangleABC(ShapeABC):
"""Rectangle MUST inherit from ShapeABC."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# ========== PROTOCOL APPROACH ==========
print("--- Protocol ---")
@runtime_checkable
class ShapeProtocol(Protocol):
"""
Protocol approach.
Classes just need matching methods.
NO inheritance required.
"""
def area(self) -> float:
...
def perimeter(self) -> float:
...
class CircleProtocol:
"""Circle WITHOUT inheritance - just has the methods."""
PI = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return CircleProtocol.PI * self.radius ** 2
def perimeter(self) -> float:
return 2 * CircleProtocol.PI * self.radius
class RectangleProtocol:
"""Rectangle WITHOUT inheritance - just has the methods."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# ========== COMPARISON ==========
print("--- Comparison ---\n")
# Function that works with ABC
def calculate_abc(shape: ShapeABC) -> None:
"""Takes ShapeABC - MUST be a subclass."""
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
# Function that works with Protocol
def calculate_protocol(shape: ShapeProtocol) -> None:
"""Takes ShapeProtocol - just needs matching methods."""
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
# Create instances
circle_abc = CircleABC(5)
rect_abc = RectangleABC(4, 6)
circle_prot = CircleProtocol(5)
rect_prot = RectangleProtocol(4, 6)
# Test ABC approach
print("ABC Shapes:")
print("Circle:")
calculate_abc(circle_abc)
print("Rectangle:")
calculate_abc(rect_abc)
print()
# Test Protocol approach
print("Protocol Shapes:")
print("Circle:")
calculate_protocol(circle_prot)
print("Rectangle:")
calculate_protocol(rect_prot)
print()
# Key difference: inheritance check
print("--- Inheritance Check ---")
print(f"CircleABC is subclass of ShapeABC: {issubclass(CircleABC, ShapeABC)}")
print(f"CircleProtocol is subclass of ShapeProtocol: {isinstance(circle_prot, ShapeProtocol)}")
# ABC enforces at class definition
print("\n--- ABC Enforcement ---")
class IncompleteABC(ShapeABC):
"""ABC enforces implementation at instantiation."""
def area(self) -> float:
return 0
# Missing perimeter()!
try:
incomplete = IncompleteABC()
except TypeError as e:
print(f"ABC Error: {e}")
# Protocol allows incomplete (type checker catches it)
print("\n--- Protocol (no enforcement at runtime) ---")
class IncompleteProtocol:
"""Protocol doesn't enforce at runtime."""
def area(self) -> float:
return 0
# Missing perimeter() - but no error at creation
incomplete_prot = IncompleteProtocol()
print(f"IncompleteProtocol created successfully")
print(f"isinstance check: {isinstance(incomplete_prot, ShapeProtocol)}")
# Cross-compatibility demonstration
print("\n--- Cross-Compatibility ---")
print("Protocol function can accept ABC classes:")
calculate_protocol(circle_abc)
print("(ABC classes satisfy Protocol if they have the methods!)")
print("\n=== When to Use Which? ===")
print("""
Use ABC when:
├─ You need runtime enforcement
├─ You want to provide default implementations
├─ You have a clear inheritance hierarchy
├─ Classes MUST explicitly inherit
└─ Example: Framework base classes, plugin systems
Use Protocol when:
├─ You want structural (duck) typing
├─ Classes from different libraries should work
├─ No inheritance relationship desired
├─ Type hints for existing code without changes
└─ Example: Accepting any "file-like" object
Can use both:
├─ Protocol for type hints (flexible)
├─ ABC for your own implementations (enforced)
""")
ABC: strict inheritance. Protocol: structural compatibility. Both valid.
Built-in protocols
Standard library protocols you use every day.
# Common Protocol Patterns and Standard Library Protocols
from typing import Protocol, Iterable, Iterator, Callable, Sized, runtime_checkable
print("=== Common Protocol Patterns ===\n")
# ========== ITERABLE PATTERN ==========
print("--- Iterable Pattern ---")
@runtime_checkable
class IterableProtocol(Protocol):
"""Protocol for objects that can be iterated."""
def __iter__(self):
...
class NumberRange:
"""Custom iterable - numbers from start to end."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
current = self.start
while current <= self.end:
yield current
current += 1
# Use in for loop
numbers = NumberRange(1, 5)
print(f"NumberRange(1, 5): {list(numbers)}")
print(f"Is Iterable: {isinstance(numbers, IterableProtocol)}")
print()
# ========== CALLABLE PATTERN ==========
print("--- Callable Pattern ---")
@runtime_checkable
class CallableProtocol(Protocol):
"""Protocol for objects that can be called like functions."""
def __call__(self, *args, **kwargs):
...
class Multiplier:
"""Callable class - multiplies by a factor."""
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(f"double(5) = {double(5)}")
print(f"triple(5) = {triple(5)}")
print(f"Is Callable: {isinstance(double, CallableProtocol)}")
# Regular functions are also callable
def square(x):
return x * x
print(f"square is Callable: {isinstance(square, CallableProtocol)}")
print()
# ========== SIZED PATTERN ==========
print("--- Sized Pattern ---")
@runtime_checkable
class SizedProtocol(Protocol):
"""Protocol for objects with length."""
def __len__(self) -> int:
...
class Playlist:
"""Sized class - has length."""
def __init__(self, name):
self.name = name
self.songs = []
def add(self, song):
self.songs.append(song)
def __len__(self) -> int:
return len(self.songs)
playlist = Playlist("My Mix")
playlist.add("Song A")
playlist.add("Song B")
playlist.add("Song C")
print(f"Playlist '{playlist.name}' has {len(playlist)} songs")
print(f"Is Sized: {isinstance(playlist, SizedProtocol)}")
print()
# ========== CONTAINER PATTERN ==========
print("--- Container Pattern (supports 'in') ---")
@runtime_checkable
class ContainerProtocol(Protocol):
"""Protocol for objects that support 'in' operator."""
def __contains__(self, item) -> bool:
...
class ShoppingCart:
"""Container class - supports 'in' operator."""
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __contains__(self, item) -> bool:
return item in self.items
cart = ShoppingCart()
cart.add("Apple")
cart.add("Banana")
print(f"'Apple' in cart: {'Apple' in cart}")
print(f"'Orange' in cart: {'Orange' in cart}")
print(f"Is Container: {isinstance(cart, ContainerProtocol)}")
print()
# ========== CONTEXT MANAGER PATTERN ==========
print("--- Context Manager Pattern ---")
@runtime_checkable
class ContextManagerProtocol(Protocol):
"""Protocol for objects that work with 'with' statement."""
def __enter__(self):
...
def __exit__(self, exc_type, exc_val, exc_tb):
...
class Timer:
"""Context manager that measures execution time."""
def __init__(self, name):
self.name = name
self.start = 0
def __enter__(self):
from time import time
self.start = time()
print(f"[{self.name}] Starting...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
from time import time
elapsed = time() - self.start
print(f"[{self.name}] Completed in {elapsed:.4f} seconds")
return False
print(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerProtocol)}")
# Use the context manager
with Timer("Processing") as t:
total = sum(range(10000))
print(f" Sum calculated: {total}")
print()
# ========== COMBINING PROTOCOLS ==========
print("--- Combining Multiple Protocols ---")
class DataStore:
"""Class implementing multiple protocols."""
def __init__(self):
self.data = []
def add(self, item):
self.data.append(item)
# Iterable
def __iter__(self):
return iter(self.data)
# Sized
def __len__(self) -> int:
return len(self.data)
# Container
def __contains__(self, item) -> bool:
return item in self.data
store = DataStore()
store.add("A")
store.add("B")
store.add("C")
print(f"DataStore: {list(store)}")
print(f"Length: {len(store)}")
print(f"'B' in store: {'B' in store}")
print("\nProtocol checks:")
print(f" Is Iterable: {isinstance(store, IterableProtocol)}")
print(f" Is Sized: {isinstance(store, SizedProtocol)}")
print(f" Is Container: {isinstance(store, ContainerProtocol)}")
print("\n=== Standard Library Protocols ===")
print("""
typing module provides many protocols:
Iterable[T] - has __iter__()
Iterator[T] - has __iter__() and __next__()
Callable[...,R] - can be called
Sized - has __len__()
Container[T] - has __contains__()
Hashable - has __hash__()
Reversible[T] - has __reversed__()
SupportsInt - has __int__()
SupportsFloat - has __float__()
SupportsAbs - has __abs__()
SupportsBytes - has __bytes__()
Example usage:
from typing import Iterable, Callable
def process(items: Iterable[str], func: Callable[[str], str]):
return [func(item) for item in items]
""")
Iterable, Callable, Sized, Hashable - all protocols.
Exercise: practical.py
Design a file-like protocol for custom storage backends