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 = [circle, rectangle, text, line]
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 = [circle, text]
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 = [rectangle, line]
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!
""")
Protocol for drawable objects.
7class Drawable(Protocol): #?drawableprotocol8 """9 Protocol for drawable objects.10 Any class with draw() method is compatible.11 """12 13 def draw(self) -> str: #?drawmethod14 """Draw the object and return description."""15 ... #?ellipsis161718# Classes that satisfy the protocol #?satisfyprotocol19# Note: NO inheritance from Drawable! #?noinheritance2021class Circle: #?circleclass22 """Circle class - has draw() method."""23 24 def __init__(self, radius): #?circleinit25 self.radius = radius26 27 def draw(self) -> str: #?circledraw28 return f"Drawing circle with radius {self.radius}"293031class Rectangle: #?rectangleclass32 """Rectangle class - has draw() method."""33 34 def __init__(self, width, height): #?rectangleinit35 self.width = width36 self.height = height37 38 def draw(self) -> str: #?rectangledraw39 return f"Drawing rectangle {self.width}x{self.height}"404142class Text: #?textclass43 """Text class - has draw() method."""44 45 def __init__(self, content): #?textinit46 self.content = content47 48 def draw(self) -> str: #?textdraw49 return f"Drawing text: '{self.content}'"505152class Line: #?lineclass53 """Line class - has draw() method."""54 55 def __init__(self, start, end): #?lineinit56 self.start = start57 self.end = end58 59 def draw(self) -> str: #?linedraw60 return f"Drawing line from {self.start} to {self.end}"616263# Function using the protocol as type hint #?usepProtocol64def render_shape(shape: Drawable) -> None: #?rendershape65 """66 Render any Drawable object.67 Works with Circle, Rectangle, Text, Line - anything with draw().68 """69 print(f"Rendering: {shape.draw()}") #?callsdraw707172def render_all(shapes: list[Drawable]) -> None: #?renderall73 """Render multiple drawable objects."""74 print("=== Rendering All Shapes ===")75 for shape in shapes: #?iterateshapes76 render_shape(shape) #?rendereachshape777879print("=== Basic Protocols ===\n")8081# Create objects (none inherit from Drawable!) #?createobjects82circle = Circle(5) #?createcircle83rectangle = Rectangle(10, 20) #?createrectangleoutput=== Basic Protocols ===self.radius ← 5
24def __init__(self⟨Circle A⟩, radius5): #?circleinit25 self.radius→ 5 = radius5circle ← ⟨Circle A⟩
81# Create objects (none inherit from Drawable!) #?createobjects82circle→ ⟨Circle A⟩ = Circle(5) #?createcircle83rectangle = Rectangle(10, 20) #?createrectangle84text = Text("Hello") #?createtextself.width ← 10, self.height ← 20
34def __init__(self⟨Rectangle B⟩, width10, height20): #?rectangleinit35 self.width→ 10 = width1036 self.height→ 20 = height20rectangle ← ⟨Rectangle B⟩
82circle = Circle(5) #?createcircle83rectangle→ ⟨Rectangle B⟩ = Rectangle(10, 20) #?createrectangle84text = Text("Hello") #?createtext85line = Line((0, 0), (10, 10)) #?createlineself.content ← Hello
45def __init__(self⟨Text C⟩, contentHello): #?textinit46 self.content→ Hello = contentHellotext ← ⟨Text C⟩
83rectangle = Rectangle(10, 20) #?createrectangle84text→ ⟨Text C⟩ = Text("Hello") #?createtext85line = Line((0, 0), (10, 10)) #?createlineself.start ← (0, 0), self.end ← (10, 10)
55def __init__(self⟨Line D⟩, start(0, 0), end(10, 10)): #?lineinit56 self.start→ (0, 0) = start(0, 0)57 self.end→ (10, 10) = end(10, 10)line ← ⟨Line D⟩
84text = Text("Hello") #?createtext85line→ ⟨Line D⟩ = Line((0, 0), (10, 10)) #?createline8687# All work with render_shape() #?renderindividual88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩) #?rendercircle90render_shape(rectangle) #?renderrectangleoutput--- Individual Rendering ---def render_shape(shape: Drawable) -> None: #?rendershape
pass 1 of 863# Function using the protocol as type hint #?usepProtocol64def render_shape(shape⟨Circle A⟩: Drawable) -> None: #?rendershape65 """66 Render any Drawable object.67 Works with Circle, Rectangle, Text, Line - anything with draw().68 """69 print(f"Rendering: {shape⟨Circle A⟩.draw()}") #?callsdrawAll 8 passes — pass 1 is the card above pass shapeselfself.radiusself.widthself.heightself.contentself.startself.end1 ⟨Circle A⟩ ⟨Circle A⟩ 5 — — — — — 2 ⟨Rectangle B⟩ ⟨Rectangle B⟩ — 10 20 — — — 3 ⟨Text C⟩ ⟨Text C⟩ — — — Hello — — 4 ⟨Line D⟩ ⟨Line D⟩ — — — — (0, 0) (10, 10) 5 ⟨Circle A⟩ ⟨Circle A⟩ 5 — — — — — 6 ⟨Rectangle B⟩ ⟨Rectangle B⟩ — 10 20 — — — 7 ⟨Text C⟩ ⟨Text C⟩ — — — Hello — — 8 ⟨Line D⟩ ⟨Line D⟩ — — — — (0, 0) (10, 10) def draw(self) -> str: #?circledraw
pass 1 of 227def draw(self⟨Circle A⟩) -> str: #?circledraw28 return f"Drawing circle with radius {self.radius5}"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}") #?callsdrawoutputRendering: Drawing circle with radius 5render_shape(circle) #?rendercircle
88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩) #?rendercircle90render_shape(rectangle⟨Rectangle B⟩) #?renderrectangle91render_shape(text) #?rendertextdef draw(self) -> str: #?rectangledraw
pass 1 of 238def draw(self⟨Rectangle B⟩) -> str: #?rectangledraw39 return f"Drawing rectangle {self.width10}x{self.height20}"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}") #?callsdrawoutputRendering: Drawing rectangle 10x20render_shape(rectangle) #?renderrectangle
89render_shape(circle) #?rendercircle90render_shape(rectangle⟨Rectangle B⟩) #?renderrectangle91render_shape(text⟨Text C⟩) #?rendertext92render_shape(line) #?renderlinedef draw(self) -> str: #?textdraw
pass 1 of 248def draw(self⟨Text C⟩) -> str: #?textdraw49 return f"Drawing text: '{self.contentHello}'"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}") #?callsdrawoutputRendering: Drawing text: 'Hello'render_shape(text) #?rendertext
90render_shape(rectangle) #?renderrectangle91render_shape(text⟨Text C⟩) #?rendertext92render_shape(line⟨Line D⟩) #?renderlinedef draw(self) -> str: #?linedraw
pass 1 of 259def draw(self⟨Line D⟩) -> str: #?linedraw60 return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}") #?callsdrawoutputRendering: Drawing line from (0, 0) to (10, 10)shapes ← [⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩]
91render_shape(text) #?rendertext92render_shape(line⟨Line D⟩) #?renderline9394print()9596# Render all together #?renderalltogether97shapes→ [⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩] = [circle⟨Circle A⟩, rectangle⟨Rectangle B⟩, text⟨Text C⟩, line⟨Line D⟩] #?shapeslist98#@shapes=[circle, text], [rectangle, line], [circle, rectangle, text, line]99render_all(shapes[⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩]) #?callrenderalldef render_all(shapes: list[Drawable]) -> None: #?renderall
72def render_all(shapes[⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩]: list[Drawable]) -> None: #?renderall73 """Render multiple drawable objects."""74 print("=== Rendering All Shapes ===")75 for shape in shapes: #?iterateshapesoutput=== Rendering All Shapes ===for shape in shapes: #?iterateshapes
pass 1 of 474print("=== Rendering All Shapes ===")75for shape⟨Circle A⟩ in shapes[⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩]: #?iterateshapes76 render_shape(shape⟨Circle A⟩) #?rendereachshapeAll 4 passes — pass 1 is the card above pass shapeselfself.radiusself.widthself.heightself.contentself.startself.end1 ⟨Circle A⟩ ⟨Circle A⟩ 5 — — — — — 2 ⟨Rectangle B⟩ ⟨Rectangle B⟩ — 10 20 — — — 3 ⟨Text C⟩ ⟨Text C⟩ — — — Hello — — 4 ⟨Line D⟩ ⟨Line D⟩ — — — — (0, 0) (10, 10) def draw(self) -> str: #?circledraw
pass 2 of 227def draw(self⟨Circle A⟩) -> str: #?circledraw28 return f"Drawing circle with radius {self.radius5}"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}") #?callsdrawoutputRendering: Drawing circle with radius 5render_shape(shape) #?rendereachshape
75for shape in shapes: #?iterateshapes76 render_shape(shape⟨Circle A⟩) #?rendereachshapedef draw(self) -> str: #?rectangledraw
pass 2 of 238def draw(self⟨Rectangle B⟩) -> str: #?rectangledraw39 return f"Drawing rectangle {self.width10}x{self.height20}"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}") #?callsdrawoutputRendering: Drawing rectangle 10x20render_shape(shape) #?rendereachshape
75for shape in shapes: #?iterateshapes76 render_shape(shape⟨Rectangle B⟩) #?rendereachshapedef draw(self) -> str: #?textdraw
pass 2 of 248def draw(self⟨Text C⟩) -> str: #?textdraw49 return f"Drawing text: '{self.contentHello}'"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}") #?callsdrawoutputRendering: Drawing text: 'Hello'render_shape(shape) #?rendereachshape
75for shape in shapes: #?iterateshapes76 render_shape(shape⟨Text C⟩) #?rendereachshapedef draw(self) -> str: #?linedraw
pass 2 of 259def draw(self⟨Line D⟩) -> str: #?linedraw60 return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"print(f"Rendering: {shape.draw()}") #?callsdraw
68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}") #?callsdrawoutputRendering: Drawing line from (0, 0) to (10, 10)render_shape(shape) #?rendereachshape
75for shape in shapes: #?iterateshapes76 render_shape(shape⟨Line D⟩) #?rendereachshaperender_all(shapes) #?callrenderall
98#@shapes=[circle, text], [rectangle, line], [circle, rectangle, text, line]99render_all(shapes[⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩]) #?callrenderall100101# Object without draw() method #?nondrawable102print("\n--- Non-Drawable Object ---")103104class Point: #?pointclass105 """Point has no draw() method."""106 107 def __init__(self, x, y): #?pointinit108 self.x = x109 self.y = y110111point = Point(5, 10) #?createpointoutput --- Non-Drawable Object ---self.x ← 5, self.y ← 10
107def __init__(self⟨Point E⟩, x5, y10): #?pointinit108 self.x→ 5 = x5109 self.y→ 10 = y10point ← ⟨Point E⟩
111point→ ⟨Point E⟩ = Point(5, 10) #?createpoint112113# This would cause a type error (if using type checker) #?typeerror114# but still runs because Python is dynamically typed #?dynamictyping115print("Point object created (no draw method)")116print("Type checkers would flag: render_shape(point)")117118# Demonstrate that it's structural, not nominal #?structural119print("\n--- Structural Typing Proof ---")120print(f"Circle inherits from Drawable: {issubclass(Circle<class '__main__.Circle'>, Drawable<class '__main__.Drawable'>) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}") #?nosubclass121122print("\n=== Protocol Key Points ===")123print("""1241. Define interface with Protocol:125 class MyProtocol(Protocol):126 def method(self) -> Type: ...1271282. No inheritance required:129 - Classes just need matching methods130 - "Structural subtyping"1311323. Works with type checkers:133 - mypy, pyright, etc.134 - Catches mismatches at development time1351364. ... (ellipsis) vs pass:137 - Both work in protocols138 - ... is convention for "to be implemented"1391405. Duck typing formalized:141 - "If it walks like a duck..."142 - Now with type hints!143""")outputPoint object created (no draw method) Type checkers would flag: render_shape(point) --- Structural Typing Proof --- Circle inherits from Drawable: True === Protocol Key Points === 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!
Protocol for drawable objects.
7class Drawable(Protocol):8 """9 Protocol for drawable objects.10 Any class with draw() method is compatible.11 """12 13 def draw(self) -> str:14 """Draw the object and return description."""15 ...161718# Classes that satisfy the protocol19# Note: NO inheritance from Drawable!2021class Circle:22 """Circle class - has draw() method."""23 24 def __init__(self, radius):25 self.radius = radius26 27 def draw(self) -> str:28 return f"Drawing circle with radius {self.radius}"293031class Rectangle:32 """Rectangle class - has draw() method."""33 34 def __init__(self, width, height):35 self.width = width36 self.height = height37 38 def draw(self) -> str:39 return f"Drawing rectangle {self.width}x{self.height}"404142class Text:43 """Text class - has draw() method."""44 45 def __init__(self, content):46 self.content = content47 48 def draw(self) -> str:49 return f"Drawing text: '{self.content}'"505152class Line:53 """Line class - has draw() method."""54 55 def __init__(self, start, end):56 self.start = start57 self.end = end58 59 def draw(self) -> str:60 return f"Drawing line from {self.start} to {self.end}"616263# Function using the protocol as type hint64def render_shape(shape: Drawable) -> None:65 """66 Render any Drawable object.67 Works with Circle, Rectangle, Text, Line - anything with draw().68 """69 print(f"Rendering: {shape.draw()}")707172def render_all(shapes: list[Drawable]) -> None:73 """Render multiple drawable objects."""74 print("=== Rendering All Shapes ===")75 for shape in shapes:76 render_shape(shape)777879print("=== Basic Protocols ===\n")8081# Create objects (none inherit from Drawable!)82circle = Circle(5)83rectangle = Rectangle(10, 20)output=== Basic Protocols ===self.radius ← 5
24def __init__(self⟨Circle A⟩, radius5):25 self.radius→ 5 = radius5circle ← ⟨Circle A⟩
81# Create objects (none inherit from Drawable!)82circle→ ⟨Circle A⟩ = Circle(5)83rectangle = Rectangle(10, 20)84text = Text("Hello")self.width ← 10, self.height ← 20
34def __init__(self⟨Rectangle B⟩, width10, height20):35 self.width→ 10 = width1036 self.height→ 20 = height20rectangle ← ⟨Rectangle B⟩
82circle = Circle(5)83rectangle→ ⟨Rectangle B⟩ = Rectangle(10, 20)84text = Text("Hello")85line = Line((0, 0), (10, 10))self.content ← Hello
45def __init__(self⟨Text C⟩, contentHello):46 self.content→ Hello = contentHellotext ← ⟨Text C⟩
83rectangle = Rectangle(10, 20)84text→ ⟨Text C⟩ = Text("Hello")85line = Line((0, 0), (10, 10))self.start ← (0, 0), self.end ← (10, 10)
55def __init__(self⟨Line D⟩, start(0, 0), end(10, 10)):56 self.start→ (0, 0) = start(0, 0)57 self.end→ (10, 10) = end(10, 10)line ← ⟨Line D⟩
84text = Text("Hello")85line→ ⟨Line D⟩ = Line((0, 0), (10, 10))8687# All work with render_shape()88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩)90render_shape(rectangle)output--- Individual Rendering ---def render_shape(shape: Drawable) -> None:
pass 1 of 663# Function using the protocol as type hint64def render_shape(shape⟨Circle A⟩: Drawable) -> None:65 """66 Render any Drawable object.67 Works with Circle, Rectangle, Text, Line - anything with draw().68 """69 print(f"Rendering: {shape⟨Circle A⟩.draw()}")All 6 passes — pass 1 is the card above pass shapeselfself.radiusself.widthself.heightself.contentself.startself.end1 ⟨Circle A⟩ ⟨Circle A⟩ 5 — — — — — 2 ⟨Rectangle B⟩ ⟨Rectangle B⟩ — 10 20 — — — 3 ⟨Text C⟩ ⟨Text C⟩ — — — Hello — — 4 ⟨Line D⟩ ⟨Line D⟩ — — — — (0, 0) (10, 10) 5 ⟨Circle A⟩ ⟨Circle A⟩ 5 — — — — — 6 ⟨Text C⟩ ⟨Text C⟩ — — — Hello — — def draw(self) -> str:
pass 1 of 227def draw(self⟨Circle A⟩) -> str:28 return f"Drawing circle with radius {self.radius5}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}")outputRendering: Drawing circle with radius 5render_shape(circle)
88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text)def draw(self) -> str:
38def draw(self⟨Rectangle B⟩) -> str:39 return f"Drawing rectangle {self.width10}x{self.height20}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}")outputRendering: Drawing rectangle 10x20render_shape(rectangle)
89render_shape(circle)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text⟨Text C⟩)92render_shape(line)def draw(self) -> str:
pass 1 of 248def draw(self⟨Text C⟩) -> str:49 return f"Drawing text: '{self.contentHello}'"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}")outputRendering: Drawing text: 'Hello'render_shape(text)
90render_shape(rectangle)91render_shape(text⟨Text C⟩)92render_shape(line⟨Line D⟩)def draw(self) -> str:
59def draw(self⟨Line D⟩) -> str:60 return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}")outputRendering: Drawing line from (0, 0) to (10, 10)shapes ← [⟨Circle A⟩, ⟨Text C⟩]
91render_shape(text)92render_shape(line⟨Line D⟩)9394print()9596# Render all together97shapes→ [⟨Circle A⟩, ⟨Text C⟩] = [circle⟨Circle A⟩, text⟨Text C⟩]98render_all(shapes[⟨Circle A⟩, ⟨Text C⟩])def render_all(shapes: list[Drawable]) -> None:
72def render_all(shapes[⟨Circle A⟩, ⟨Text C⟩]: list[Drawable]) -> None:73 """Render multiple drawable objects."""74 print("=== Rendering All Shapes ===")75 for shape in shapes:output=== Rendering All Shapes ===for shape in shapes:
pass 1 of 274print("=== Rendering All Shapes ===")75for shape⟨Circle A⟩ in shapes[⟨Circle A⟩, ⟨Text C⟩]:76 render_shape(shape⟨Circle A⟩)def draw(self) -> str:
pass 2 of 227def draw(self⟨Circle A⟩) -> str:28 return f"Drawing circle with radius {self.radius5}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}")outputRendering: Drawing circle with radius 5render_shape(shape)
75for shape in shapes:76 render_shape(shape⟨Circle A⟩)for shape in shapes:
pass 2 of 274print("=== Rendering All Shapes ===")75for shape⟨Text C⟩ in shapes[⟨Circle A⟩, ⟨Text C⟩]:76 render_shape(shape⟨Text C⟩)def draw(self) -> str:
pass 2 of 248def draw(self⟨Text C⟩) -> str:49 return f"Drawing text: '{self.contentHello}'"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}")outputRendering: Drawing text: 'Hello'render_shape(shape)
75for shape in shapes:76 render_shape(shape⟨Text C⟩)render_all(shapes)
97shapes = [circle, text]98render_all(shapes[⟨Circle A⟩, ⟨Text C⟩])99100# Object without draw() method101print("\n--- Non-Drawable Object ---")102103class Point:104 """Point has no draw() method."""105 106 def __init__(self, x, y):107 self.x = x108 self.y = y109110point = Point(5, 10)output --- Non-Drawable Object ---self.x ← 5, self.y ← 10
106def __init__(self⟨Point E⟩, x5, y10):107 self.x→ 5 = x5108 self.y→ 10 = y10point ← ⟨Point E⟩
110point→ ⟨Point E⟩ = Point(5, 10)111112# This would cause a type error (if using type checker)113# but still runs because Python is dynamically typed114print("Point object created (no draw method)")115print("Type checkers would flag: render_shape(point)")116117# Demonstrate that it's structural, not nominal118print("\n--- Structural Typing Proof ---")119print(f"Circle inherits from Drawable: {issubclass(Circle<class '__main__.Circle'>, Drawable<class '__main__.Drawable'>) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")120121print("\n=== Protocol Key Points ===")122print("""1231. Define interface with Protocol:124 class MyProtocol(Protocol):125 def method(self) -> Type: ...1261272. No inheritance required:128 - Classes just need matching methods129 - "Structural subtyping"1301313. Works with type checkers:132 - mypy, pyright, etc.133 - Catches mismatches at development time1341354. ... (ellipsis) vs pass:136 - Both work in protocols137 - ... is convention for "to be implemented"1381395. Duck typing formalized:140 - "If it walks like a duck..."141 - Now with type hints!142""")outputPoint object created (no draw method) Type checkers would flag: render_shape(point) --- Structural Typing Proof --- Circle inherits from Drawable: True === Protocol Key Points === 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!
Protocol for drawable objects.
7class Drawable(Protocol):8 """9 Protocol for drawable objects.10 Any class with draw() method is compatible.11 """12 13 def draw(self) -> str:14 """Draw the object and return description."""15 ...161718# Classes that satisfy the protocol19# Note: NO inheritance from Drawable!2021class Circle:22 """Circle class - has draw() method."""23 24 def __init__(self, radius):25 self.radius = radius26 27 def draw(self) -> str:28 return f"Drawing circle with radius {self.radius}"293031class Rectangle:32 """Rectangle class - has draw() method."""33 34 def __init__(self, width, height):35 self.width = width36 self.height = height37 38 def draw(self) -> str:39 return f"Drawing rectangle {self.width}x{self.height}"404142class Text:43 """Text class - has draw() method."""44 45 def __init__(self, content):46 self.content = content47 48 def draw(self) -> str:49 return f"Drawing text: '{self.content}'"505152class Line:53 """Line class - has draw() method."""54 55 def __init__(self, start, end):56 self.start = start57 self.end = end58 59 def draw(self) -> str:60 return f"Drawing line from {self.start} to {self.end}"616263# Function using the protocol as type hint64def render_shape(shape: Drawable) -> None:65 """66 Render any Drawable object.67 Works with Circle, Rectangle, Text, Line - anything with draw().68 """69 print(f"Rendering: {shape.draw()}")707172def render_all(shapes: list[Drawable]) -> None:73 """Render multiple drawable objects."""74 print("=== Rendering All Shapes ===")75 for shape in shapes:76 render_shape(shape)777879print("=== Basic Protocols ===\n")8081# Create objects (none inherit from Drawable!)82circle = Circle(5)83rectangle = Rectangle(10, 20)output=== Basic Protocols ===self.radius ← 5
24def __init__(self⟨Circle A⟩, radius5):25 self.radius→ 5 = radius5circle ← ⟨Circle A⟩
81# Create objects (none inherit from Drawable!)82circle→ ⟨Circle A⟩ = Circle(5)83rectangle = Rectangle(10, 20)84text = Text("Hello")self.width ← 10, self.height ← 20
34def __init__(self⟨Rectangle B⟩, width10, height20):35 self.width→ 10 = width1036 self.height→ 20 = height20rectangle ← ⟨Rectangle B⟩
82circle = Circle(5)83rectangle→ ⟨Rectangle B⟩ = Rectangle(10, 20)84text = Text("Hello")85line = Line((0, 0), (10, 10))self.content ← Hello
45def __init__(self⟨Text C⟩, contentHello):46 self.content→ Hello = contentHellotext ← ⟨Text C⟩
83rectangle = Rectangle(10, 20)84text→ ⟨Text C⟩ = Text("Hello")85line = Line((0, 0), (10, 10))self.start ← (0, 0), self.end ← (10, 10)
55def __init__(self⟨Line D⟩, start(0, 0), end(10, 10)):56 self.start→ (0, 0) = start(0, 0)57 self.end→ (10, 10) = end(10, 10)line ← ⟨Line D⟩
84text = Text("Hello")85line→ ⟨Line D⟩ = Line((0, 0), (10, 10))8687# All work with render_shape()88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩)90render_shape(rectangle)output--- Individual Rendering ---def render_shape(shape: Drawable) -> None:
pass 1 of 663# Function using the protocol as type hint64def render_shape(shape⟨Circle A⟩: Drawable) -> None:65 """66 Render any Drawable object.67 Works with Circle, Rectangle, Text, Line - anything with draw().68 """69 print(f"Rendering: {shape⟨Circle A⟩.draw()}")All 6 passes — pass 1 is the card above pass shapeselfself.radiusself.widthself.heightself.contentself.startself.end1 ⟨Circle A⟩ ⟨Circle A⟩ 5 — — — — — 2 ⟨Rectangle B⟩ ⟨Rectangle B⟩ — 10 20 — — — 3 ⟨Text C⟩ ⟨Text C⟩ — — — Hello — — 4 ⟨Line D⟩ ⟨Line D⟩ — — — — (0, 0) (10, 10) 5 ⟨Rectangle B⟩ ⟨Rectangle B⟩ — 10 20 — — — 6 ⟨Line D⟩ ⟨Line D⟩ — — — — (0, 0) (10, 10) def draw(self) -> str:
27def draw(self⟨Circle A⟩) -> str:28 return f"Drawing circle with radius {self.radius5}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}")outputRendering: Drawing circle with radius 5render_shape(circle)
88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text)def draw(self) -> str:
pass 1 of 238def draw(self⟨Rectangle B⟩) -> str:39 return f"Drawing rectangle {self.width10}x{self.height20}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}")outputRendering: Drawing rectangle 10x20render_shape(rectangle)
89render_shape(circle)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text⟨Text C⟩)92render_shape(line)def draw(self) -> str:
48def draw(self⟨Text C⟩) -> str:49 return f"Drawing text: '{self.contentHello}'"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}")outputRendering: Drawing text: 'Hello'render_shape(text)
90render_shape(rectangle)91render_shape(text⟨Text C⟩)92render_shape(line⟨Line D⟩)def draw(self) -> str:
pass 1 of 259def draw(self⟨Line D⟩) -> str:60 return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}")outputRendering: Drawing line from (0, 0) to (10, 10)shapes ← [⟨Rectangle B⟩, ⟨Line D⟩]
91render_shape(text)92render_shape(line⟨Line D⟩)9394print()9596# Render all together97shapes→ [⟨Rectangle B⟩, ⟨Line D⟩] = [rectangle⟨Rectangle B⟩, line⟨Line D⟩]98render_all(shapes[⟨Rectangle B⟩, ⟨Line D⟩])def render_all(shapes: list[Drawable]) -> None:
72def render_all(shapes[⟨Rectangle B⟩, ⟨Line D⟩]: list[Drawable]) -> None:73 """Render multiple drawable objects."""74 print("=== Rendering All Shapes ===")75 for shape in shapes:output=== Rendering All Shapes ===for shape in shapes:
pass 1 of 274print("=== Rendering All Shapes ===")75for shape⟨Rectangle B⟩ in shapes[⟨Rectangle B⟩, ⟨Line D⟩]:76 render_shape(shape⟨Rectangle B⟩)def draw(self) -> str:
pass 2 of 238def draw(self⟨Rectangle B⟩) -> str:39 return f"Drawing rectangle {self.width10}x{self.height20}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}")outputRendering: Drawing rectangle 10x20render_shape(shape)
75for shape in shapes:76 render_shape(shape⟨Rectangle B⟩)for shape in shapes:
pass 2 of 274print("=== Rendering All Shapes ===")75for shape⟨Line D⟩ in shapes[⟨Rectangle B⟩, ⟨Line D⟩]:76 render_shape(shape⟨Line D⟩)def draw(self) -> str:
pass 2 of 259def draw(self⟨Line D⟩) -> str:60 return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"print(f"Rendering: {shape.draw()}")
68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}")outputRendering: Drawing line from (0, 0) to (10, 10)render_shape(shape)
75for shape in shapes:76 render_shape(shape⟨Line D⟩)render_all(shapes)
97shapes = [rectangle, line]98render_all(shapes[⟨Rectangle B⟩, ⟨Line D⟩])99100# Object without draw() method101print("\n--- Non-Drawable Object ---")102103class Point:104 """Point has no draw() method."""105 106 def __init__(self, x, y):107 self.x = x108 self.y = y109110point = Point(5, 10)output --- Non-Drawable Object ---self.x ← 5, self.y ← 10
106def __init__(self⟨Point E⟩, x5, y10):107 self.x→ 5 = x5108 self.y→ 10 = y10point ← ⟨Point E⟩
110point→ ⟨Point E⟩ = Point(5, 10)111112# This would cause a type error (if using type checker)113# but still runs because Python is dynamically typed114print("Point object created (no draw method)")115print("Type checkers would flag: render_shape(point)")116117# Demonstrate that it's structural, not nominal118print("\n--- Structural Typing Proof ---")119print(f"Circle inherits from Drawable: {issubclass(Circle<class '__main__.Circle'>, Drawable<class '__main__.Drawable'>) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")120121print("\n=== Protocol Key Points ===")122print("""1231. Define interface with Protocol:124 class MyProtocol(Protocol):125 def method(self) -> Type: ...1261272. No inheritance required:128 - Classes just need matching methods129 - "Structural subtyping"1301313. Works with type checkers:132 - mypy, pyright, etc.133 - Catches mismatches at development time1341354. ... (ellipsis) vs pass:136 - Both work in protocols137 - ... is convention for "to be implemented"1381395. Duck typing formalized:140 - "If it walks like a duck..."141 - Now with type hints!142""")outputPoint object created (no draw method) Type checkers would flag: render_shape(point) --- Structural Typing Proof --- Circle inherits from Drawable: True === Protocol Key Points === 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)
""")
brand: str #?brandattr
6class Vehicle(Protocol): #?vehicleprotocol7 """8 Protocol for vehicles.9 Requires specific attributes AND methods.10 """11 12 # Attribute declarations #?attributedeclarations13 brand(empty): str #?brandattr14 model(empty): str #?modelattr15 year(empty): int #?yearattr16 17 # Method declarations #?methoddeclarations18 def start(self) -> str: #?startmethod19 """Start the vehicle."""20 ...21 22 def stop(self) -> str: #?stopmethod23 """Stop the vehicle."""24 ...25 26 def get_info(self) -> str: #?getinfomethod27 """Get vehicle info."""28 ...293031# Classes implementing the protocol #?implementprotocol3233class Car: #?carclass34 """Car satisfies Vehicle protocol."""35 36 def __init__(self, brand: str, model: str, year: int): #?carinit37 self.brand = brand # Required attribute #?carbrand38 self.model = model # Required attribute #?carmodel39 self.year = year # Required attribute #?caryear40 self._running = False #?runningstate41 42 def start(self) -> str: #?carstart43 self._running = True44 return f"{self.brand} {self.model} engine started"45 46 def stop(self) -> str: #?carstop47 self._running = False48 return f"{self.brand} {self.model} engine stopped"49 50 def get_info(self) -> str: #?cargetinfo51 status = "running" if self._running else "stopped"52 return f"{self.year} {self.brand} {self.model} ({status})"535455class Motorcycle: #?motorcycleclass56 """Motorcycle satisfies Vehicle protocol."""57 58 def __init__(self, brand: str, model: str, year: int): #?motorcycleinit59 self.brand = brand60 self.model = model61 self.year = year62 self._running = False63 64 def start(self) -> str: #?motorcyclestart65 self._running = True66 return f"{self.brand} {self.model} roars to life!"67 68 def stop(self) -> str: #?motorcyclestop69 self._running = False70 return f"{self.brand} {self.model} goes silent"71 72 def get_info(self) -> str: #?motorcyclegetinfo73 status = "running" if self._running else "parked"74 return f"{self.year} {self.brand} {self.model} ({status})"757677class ElectricScooter: #?scooterclass78 """Electric scooter - also satisfies Vehicle protocol."""79 80 def __init__(self, brand: str, model: str, year: int): #?scooterinit81 self.brand = brand82 self.model = model83 self.year = year84 self._powered = False #?poweredstate85 self.battery_level = 100 #?batterylevel86 87 def start(self) -> str: #?scooterstart88 self._powered = True89 return f"{self.brand} {self.model} silently powers on"90 91 def stop(self) -> str: #?scooterstop92 self._powered = False93 return f"{self.brand} {self.model} powers off"94 95 def get_info(self) -> str: #?scootergetinfo96 status = "on" if self._powered else "off"97 return f"{self.year} {self.brand} {self.model} ({status}, {self.battery_level}% battery)"9899100# Functions using Vehicle protocol #?usevehicleprotocol101102def test_drive(vehicle: Vehicle) -> None: #?testdrive103 """Test drive any vehicle."""104 print(f"Testing: {vehicle.get_info()}") #?printinfo105 print(f" → {vehicle.start()}") #?printstart106 print(f" → {vehicle.stop()}") #?printstop107108109def vehicle_summary(vehicles: list[Vehicle]) -> None: #?vehiclesummary110 """Print summary of all vehicles."""111 print("\n=== Vehicle Fleet ===")112 for v in vehicles: #?iteratevehicles113 print(f" {v.year} {v.brand} {v.model}") #?accessattributes114115116def find_by_brand(vehicles: list[Vehicle], brand: str) -> list[Vehicle]: #?findbybrand117 """Find vehicles by brand."""118 return [v for v in vehicles if v.brand.lower() == brand.lower()] #?filterbybrand119120121print("=== Protocols with Attributes ===\n")122123# Create vehicles #?createvehicles124car = Car("Toyota", "Camry", 2022) #?createcar125motorcycle = Motorcycle("Harley-Davidson", "Street 750", 2021) #?createmotorcycleoutput=== Protocols with Attributes ===self.brand ← Toyota, self.model ← Camry, self.year ← 2022, self._running ← False
36def __init__(self⟨Car A⟩, brandToyota: str, modelCamry: str, year2022: int): #?carinit37 self.brand→ Toyota = brandToyota # Required attribute #?carbrand38 self.model→ Camry = modelCamry # Required attribute #?carmodel39 self.year→ 2022 = year2022 # Required attribute #?caryear40 self._running→ False = False #?runningstatecar ← ⟨Car A⟩
123# Create vehicles #?createvehicles124car→ ⟨Car A⟩ = Car("Toyota", "Camry", 2022) #?createcar125motorcycle = Motorcycle("Harley-Davidson", "Street 750", 2021) #?createmotorcycle126scooter = ElectricScooter("Xiaomi", "Mi Electric", 2023) #?createscooterself.brand ← Harley-Davidson, self.model ← Street 750, self.year ← 2021
58def __init__(self⟨Motorcycle B⟩, brandHarley-Davidson: str, modelStreet 750: str, year2021: int): #?motorcycleinit59 self.brand→ Harley-Davidson = brandHarley-Davidson60 self.model→ Street 750 = modelStreet 75061 self.year→ 2021 = year202162 self._running→ False = Falsemotorcycle ← ⟨Motorcycle B⟩
124car = Car("Toyota", "Camry", 2022) #?createcar125motorcycle→ ⟨Motorcycle B⟩ = Motorcycle("Harley-Davidson", "Street 750", 2021) #?createmotorcycle126scooter = ElectricScooter("Xiaomi", "Mi Electric", 2023) #?createscooterself.brand ← Xiaomi, self.model ← Mi Electric, self.year ← 2023
80def __init__(self⟨ElectricScooter C⟩, brandXiaomi: str, modelMi Electric: str, year2023: int): #?scooterinit81 self.brand→ Xiaomi = brandXiaomi82 self.model→ Mi Electric = modelMi Electric83 self.year→ 2023 = year202384 self._powered→ False = False #?poweredstate85 self.battery_level→ 100 = 100 #?batterylevelscooter ← ⟨ElectricScooter C⟩, vehicles ← [⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]
125motorcycle = Motorcycle("Harley-Davidson", "Street 750", 2021) #?createmotorcycle126scooter→ ⟨ElectricScooter C⟩ = ElectricScooter("Xiaomi", "Mi Electric", 2023) #?createscooter127128vehicles→ [⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩] = [car⟨Car A⟩, motorcycle⟨Motorcycle B⟩, scooter⟨ElectricScooter C⟩] #?vehicleslist129130# Test drive each #?testdriveall131print("--- Test Drives ---")132for v in vehicles: #?loopvehiclesoutput--- Test Drives ---for v in vehicles: #?loopvehicles
pass 1 of 3131print("--- Test Drives ---")132for v⟨Car A⟩ in vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]: #?loopvehicles133 test_drive(v⟨Car A⟩) #?calltestdrive134 print()All 3 passes — pass 1 is the card above pass vselfself._runningself.yearself.brandself.modelself._poweredself.battery_levelstatus1 ⟨Car A⟩ ⟨Car A⟩ False 2022 Toyota Camry — — stopped 2 ⟨Motorcycle B⟩ ⟨Motorcycle B⟩ False 2021 Harley-Davidson Street 750 — — parked 3 ⟨ElectricScooter C⟩ ⟨ElectricScooter C⟩ — 2023 Xiaomi Mi Electric False 100 off def test_drive(vehicle: Vehicle) -> None: #?testdrive
pass 1 of 3102def test_drive(vehicle⟨Car A⟩: Vehicle) -> None: #?testdrive103 """Test drive any vehicle."""104 print(f"Testing: {vehicle⟨Car A⟩.get_info()}") #?printinfo105 print(f" → {vehicle.start()}") #?printstartAll 3 passes — pass 1 is the card above pass vehicleselfself._runningself.yearself.brandself.modelself._poweredself.battery_levelstatus1 ⟨Car A⟩ ⟨Car A⟩ False 2022 Toyota Camry — — stopped 2 ⟨Motorcycle B⟩ ⟨Motorcycle B⟩ False 2021 Harley-Davidson Street 750 — — parked 3 ⟨ElectricScooter C⟩ ⟨ElectricScooter C⟩ — 2023 Xiaomi Mi Electric False 100 off status ← stopped
50def get_info(self⟨Car A⟩) -> str: #?cargetinfo51 status→ stopped = "running" if self._runningFalse else "stopped"52 return f"{self.year2022} {self.brandToyota} {self.modelCamry} ({statusstopped})"print(f"Testing: {vehicle.get_info()}") #?printinfo
103"""Test drive any vehicle."""104print(f"Testing: {vehicle⟨Car A⟩.get_info()}") #?printinfo105print(f" → {vehicle⟨Car A⟩.start()}") #?printstart106print(f" → {vehicle.stop()}") #?printstopoutputTesting: 2022 Toyota Camry (stopped)self._running ← True
42def start(self⟨Car A⟩) -> str: #?carstart43 self._running→ True = True44 return f"{self.brandToyota} {self.modelCamry} engine started"print(f" → {vehicle.start()}") #?printstart
104print(f"Testing: {vehicle.get_info()}") #?printinfo105print(f" → {vehicle⟨Car A⟩.start()}") #?printstart106print(f" → {vehicle⟨Car A⟩.stop()}") #?printstopoutput → Toyota Camry engine startedself._running ← False
46def stop(self⟨Car A⟩) -> str: #?carstop47 self._running→ False = False48 return f"{self.brandToyota} {self.modelCamry} engine stopped"print(f" → {vehicle.stop()}") #?printstop
105print(f" → {vehicle.start()}") #?printstart106print(f" → {vehicle⟨Car A⟩.stop()}") #?printstopoutput → Toyota Camry engine stoppedtest_drive(v) #?calltestdrive
132for v in vehicles: #?loopvehicles133 test_drive(v⟨Car A⟩) #?calltestdrive134 print()status ← parked
72def get_info(self⟨Motorcycle B⟩) -> str: #?motorcyclegetinfo73 status→ parked = "running" if self._runningFalse else "parked"74 return f"{self.year2021} {self.brandHarley-Davidson} {self.modelStreet 750} ({statusparked})"print(f"Testing: {vehicle.get_info()}") #?printinfo
103"""Test drive any vehicle."""104print(f"Testing: {vehicle⟨Motorcycle B⟩.get_info()}") #?printinfo105print(f" → {vehicle⟨Motorcycle B⟩.start()}") #?printstart106print(f" → {vehicle.stop()}") #?printstopoutputTesting: 2021 Harley-Davidson Street 750 (parked)self._running ← True
64def start(self⟨Motorcycle B⟩) -> str: #?motorcyclestart65 self._running→ True = True66 return f"{self.brandHarley-Davidson} {self.modelStreet 750} roars to life!"print(f" → {vehicle.start()}") #?printstart
104print(f"Testing: {vehicle.get_info()}") #?printinfo105print(f" → {vehicle⟨Motorcycle B⟩.start()}") #?printstart106print(f" → {vehicle⟨Motorcycle B⟩.stop()}") #?printstopoutput → Harley-Davidson Street 750 roars to life!self._running ← False
68def stop(self⟨Motorcycle B⟩) -> str: #?motorcyclestop69 self._running→ False = False70 return f"{self.brandHarley-Davidson} {self.modelStreet 750} goes silent"print(f" → {vehicle.stop()}") #?printstop
105print(f" → {vehicle.start()}") #?printstart106print(f" → {vehicle⟨Motorcycle B⟩.stop()}") #?printstopoutput → Harley-Davidson Street 750 goes silenttest_drive(v) #?calltestdrive
132for v in vehicles: #?loopvehicles133 test_drive(v⟨Motorcycle B⟩) #?calltestdrive134 print()status ← off
95def get_info(self⟨ElectricScooter C⟩) -> str: #?scootergetinfo96 status→ off = "on" if self._poweredFalse else "off"97 return f"{self.year2023} {self.brandXiaomi} {self.modelMi Electric} ({statusoff}, {self.battery_level100}% battery)"print(f"Testing: {vehicle.get_info()}") #?printinfo
103"""Test drive any vehicle."""104print(f"Testing: {vehicle⟨ElectricScooter C⟩.get_info()}") #?printinfo105print(f" → {vehicle⟨ElectricScooter C⟩.start()}") #?printstart106print(f" → {vehicle.stop()}") #?printstopoutputTesting: 2023 Xiaomi Mi Electric (off, 100% battery)self._powered ← True
87def start(self⟨ElectricScooter C⟩) -> str: #?scooterstart88 self._powered→ True = True89 return f"{self.brandXiaomi} {self.modelMi Electric} silently powers on"print(f" → {vehicle.start()}") #?printstart
104print(f"Testing: {vehicle.get_info()}") #?printinfo105print(f" → {vehicle⟨ElectricScooter C⟩.start()}") #?printstart106print(f" → {vehicle⟨ElectricScooter C⟩.stop()}") #?printstopoutput → Xiaomi Mi Electric silently powers onself._powered ← False
91def stop(self⟨ElectricScooter C⟩) -> str: #?scooterstop92 self._powered→ False = False93 return f"{self.brandXiaomi} {self.modelMi Electric} powers off"print(f" → {vehicle.stop()}") #?printstop
105print(f" → {vehicle.start()}") #?printstart106print(f" → {vehicle⟨ElectricScooter C⟩.stop()}") #?printstopoutput → Xiaomi Mi Electric powers offtest_drive(v) #?calltestdrive
132for v in vehicles: #?loopvehicles133 test_drive(v⟨ElectricScooter C⟩) #?calltestdrive134 print()vehicle_summary(vehicles) #?callsummary
136# Summary - accessing protocol attributes #?summaryDemo137vehicle_summary(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]) #?callsummarydef vehicle_summary(vehicles: list[Vehicle]) -> None: #?vehiclesummary
109def vehicle_summary(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]: list[Vehicle]) -> None: #?vehiclesummary110 """Print summary of all vehicles."""111 print("\n=== Vehicle Fleet ===")112 for v in vehicles: #?iteratevehiclesoutput === Vehicle Fleet ===for v in vehicles: #?iteratevehicles
pass 1 of 3111print("\n=== Vehicle Fleet ===")112for v⟨Car A⟩ in vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]: #?iteratevehicles113 print(f" {v.year2022} {v.brandToyota} {v.modelCamry}") #?accessattributesoutput 2022 Toyota CamryAll 3 passes — pass 1 is the card above pass vv.yearv.brandv.model1 ⟨Car A⟩ 2022 Toyota Camry 2 ⟨Motorcycle B⟩ 2021 Harley-Davidson Street 750 3 ⟨ElectricScooter C⟩ 2023 Xiaomi Mi Electric vehicle_summary(vehicles) #?callsummary
136# Summary - accessing protocol attributes #?summaryDemo137vehicle_summary(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]) #?callsummary138139# Search by brand #?searchdemo140print("\n--- Search by Brand ---")141toyotas = find_by_brand(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩], "toyota") #?searchtoyota142print(f"Toyota vehicles: {[f'{v.model}' for v in toyotas]}")output --- Search by Brand ---def find_by_brand(vehicles: list[Vehicle], brand: str) -> list[Vehicle…
116def find_by_brand(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]: list[Vehicle], brandtoyota: str) -> list[Vehicle]: #?findbybrand117 """Find vehicles by brand."""118 return [v for v in vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩] if v.brandXiaomi.lower() == brandtoyota.lower()] #?filterbybrandtoyotas ← [⟨Car A⟩]
140print("\n--- Search by Brand ---")141toyotas→ [⟨Car A⟩] = find_by_brand(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩], "toyota") #?searchtoyota142print(f"Toyota vehicles: {[f'{v.modelMi Electric}' for v in toyotas[⟨Car A⟩]]}")143144# Incomplete implementation #?incompletedemo145print("\n--- Incomplete Implementation ---")146147class Bicycle: #?bicycleclass148 """Bicycle is missing some protocol requirements."""149 150 def __init__(self, brand): #?bicycleinit151 self.brand = brand152 # Missing: model, year attributes #?missingattrs153 154 def start(self) -> str: #?bicyclestart155 return "Start pedaling"156 157 # Missing: stop(), get_info() methods #?missingmethods158159bicycle = Bicycle("Trek") #?createbicycle160print(f"Bicycle created: {bicycle.brand}")outputToyota vehicles: ['Camry'] --- Incomplete Implementation ---self.brand ← Trek
150def __init__(self⟨Bicycle D⟩, brandTrek): #?bicycleinit151 self.brand→ Trek = brandTrek152 # Missing: model, year attributes #?missingattrsbicycle ← ⟨Bicycle D⟩
159bicycle→ ⟨Bicycle D⟩ = Bicycle("Trek") #?createbicycle160print(f"Bicycle created: {bicycle.brandTrek}")161print("Type checker would flag Bicycle as not satisfying Vehicle protocol")162print("Missing: model, year, stop(), get_info()")163164print("\n=== Protocol Attribute Rules ===")165print("""1661. Attribute declarations in Protocol:167 class MyProtocol(Protocol):168 name: str # Required attribute169 value: int # Required attribute170 1712. Implementing classes need:172 - Same attribute names173 - Same types (for type checkers)174 1753. Attributes can have default values in implementations176 1774. Protocol just checks structure:178 - Has the attribute? ✓179 - Has the method? ✓180 - Correct types? ✓ (type checker)181""")outputBicycle created: Trek Type checker would flag Bicycle as not satisfying Vehicle protocol Missing: model, year, stop(), get_info() === Protocol Attribute Rules === 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
""")
dog ← ⟨Dog A⟩, fish ← ⟨Fish B⟩, duck ← ⟨Duck C⟩, robot ← ⟨Robot D⟩
6class Speakable(Protocol): #?speakableprotocol7 """Protocol without @runtime_checkable."""8 9 def speak(self) -> str: #?speakmethod10 ...111213# Runtime checkable protocol #?runtimecheckableprotocol14@runtime_checkable #?runtimedecorator15class Walkable(Protocol): #?walkableprotocol16 """17 Protocol with @runtime_checkable.18 Can use isinstance() with this protocol.19 """20 21 def walk(self) -> str: #?walkmethod22 ...232425@runtime_checkable #?swimdecorator26class Swimmable(Protocol): #?swimmableprotocol27 """Another runtime checkable protocol."""28 29 def swim(self) -> str: #?swimmethod30 ...313233# Classes implementing protocols #?implementations34class Dog: #?dogclass35 """Dog can walk and speak."""36 37 def walk(self) -> str: #?dogwalk38 return "Dog walks on four legs"39 40 def speak(self) -> str: #?dogspeak41 return "Woof!"424344class Fish: #?fishclass45 """Fish can only swim."""46 47 def swim(self) -> str: #?fishswim48 return "Fish swims with fins"495051class Duck: #?duckclass52 """Duck can do everything!"""53 54 def walk(self) -> str: #?duckwalk55 return "Duck waddles"56 57 def swim(self) -> str: #?duckswim58 return "Duck paddles on water"59 60 def speak(self) -> str: #?duckspeak61 return "Quack!"626364class Robot: #?robotclass65 """Robot can walk."""66 67 def walk(self) -> str: #?robotwalk68 return "Robot walks mechanically"697071print("=== Runtime Checkable Protocols ===\n")7273# Create objects #?createobjects74dog→ ⟨Dog A⟩ = Dog() #?createdog75fish→ ⟨Fish B⟩ = Fish() #?createfish76duck→ ⟨Duck C⟩ = Duck() #?createduck77robot→ ⟨Robot D⟩ = Robot() #?createrobot7879objects→ [⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩] = [dog⟨Dog A⟩, fish⟨Fish B⟩, duck⟨Duck C⟩, robot⟨Robot D⟩] #?objectslist8081# Test with runtime checkable protocol #?testruntimecheckable82print("--- isinstance() with @runtime_checkable ---")83print("\nWalkable check (has walk() method?):")84for obj in objects: #?iterateobjectsoutput=== Runtime Checkable Protocols === --- isinstance() with @runtime_checkable --- Walkable check (has walk() method?):name ← Dog, is_walkable ← True
pass 1 of 483print("\nWalkable check (has walk() method?):")84for obj⟨Dog A⟩ in objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]: #?iterateobjects85 name→ Dog = type(obj⟨Dog A⟩).__name__ #?getname86 is_walkable→ True = isinstance(obj⟨Dog A⟩, Walkable<class '__main__.Walkable'>) #?checkwalkable87 print(f" {nameDog}: isinstance(obj, Walkable) = {is_walkableTrue}")output Dog: isinstance(obj, Walkable) = TrueAll 4 passes — pass 1 is the card above pass objnameis_walkable1 ⟨Dog A⟩ Dog True 2 ⟨Fish B⟩ Fish False 3 ⟨Duck C⟩ Duck True 4 ⟨Robot D⟩ Robot True print(" Swimmable check (has swim() method?):")
89print("\nSwimmable check (has swim() method?):")90for obj in objects:output Swimmable check (has swim() method?):name ← Dog, is_swimmable ← False
pass 1 of 489print("\nSwimmable check (has swim() method?):")90for obj⟨Dog A⟩ in objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]:91 name→ Dog = type(obj⟨Dog A⟩).__name__92 is_swimmable→ False = isinstance(obj⟨Dog A⟩, Swimmable<class '__main__.Swimmable'>) #?checkswimmable93 print(f" {nameDog}: isinstance(obj, Swimmable) = {is_swimmableFalse}")output Dog: isinstance(obj, Swimmable) = FalseAll 4 passes — pass 1 is the card above pass objnameis_swimmable1 ⟨Dog A⟩ Dog False 2 ⟨Fish B⟩ Fish True 3 ⟨Duck C⟩ Duck True 4 ⟨Robot D⟩ Robot False print(" --- isinstance() without @runtime_checkable ---")
95# Test with non-runtime-checkable protocol #?testregular96print("\n--- isinstance() without @runtime_checkable ---")97try:output --- isinstance() without @runtime_checkable ---try:
96print("\n--- isinstance() without @runtime_checkable ---")97try:98 result = isinstance(dog⟨Dog A⟩, Speakable<class '__main__.Speakable'>) #?checkspeakable99 print(f"Dog isinstance(Speakable) = {result}")except TypeError as e: #?typeerror
99 print(f"Dog isinstance(Speakable) = {result}")100except TypeError as e: #?typeerror101 print(f"Error: {eInstance and class checks can only be used with @runtime_checkable protocols}")102 print("Cannot use isinstance() with non-runtime-checkable protocol!")outputError: Instance and class checks can only be used with @runtime_checkable protocols Cannot use isinstance() with non-runtime-checkable protocol!walkers = get_walkers(objects) #?callgetwalkers
104# Practical use: filtering by capability #?practicalfiltering105print("\n--- Filtering by Capability ---")106107def get_walkers(items: list) -> list[Walkable]: #?getwalkers108 """Filter items that can walk."""109 return [item for item in items if isinstance(item, Walkable)] #?filterwalkers110111def get_swimmers(items: list) -> list[Swimmable]: #?getswimmers112 """Filter items that can swim."""113 return [item for item in items if isinstance(item, Swimmable)] #?filterswimmers114115walkers = get_walkers(objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]) #?callgetwalkers116print(f"Walkers: {[type(w).__name__ for w in walkers]}")output --- Filtering by Capability ---def get_walkers(items: list) -> list[Walkable]: #?getwalkers
107def get_walkers(items[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]: list) -> list[Walkable]: #?getwalkers108 """Filter items that can walk."""109 return [item for item in items[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩] if isinstance(item, Walkable<class '__main__.Walkable'>)] #?filterwalkerswalkers ← [⟨Dog A⟩, ⟨Duck C⟩, ⟨Robot D⟩]
115walkers→ [⟨Dog A⟩, ⟨Duck C⟩, ⟨Robot D⟩] = get_walkers(objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]) #?callgetwalkers116print(f"Walkers: {[type(w).__name__ for w in walkers[⟨Dog A⟩, ⟨Duck C⟩, ⟨Robot D⟩]]}")117118swimmers = get_swimmers(objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]) #?callgetswimmers119print(f"Swimmers: {[type(s).__name__ for s in swimmers]}")outputWalkers: ['Dog', 'Duck', 'Robot']def get_swimmers(items: list) -> list[Swimmable]: #?getswimmers
111def get_swimmers(items[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]: list) -> list[Swimmable]: #?getswimmers112 """Filter items that can swim."""113 return [item for item in items[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩] if isinstance(item, Swimmable<class '__main__.Swimmable'>)] #?filterswimmersswimmers ← [⟨Fish B⟩, ⟨Duck C⟩]
118swimmers→ [⟨Fish B⟩, ⟨Duck C⟩] = get_swimmers(objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]) #?callgetswimmers119print(f"Swimmers: {[type(s).__name__ for s in swimmers[⟨Fish B⟩, ⟨Duck C⟩]]}")120121# Process only matching objects #?processmatching122print("\n--- Process Only Walkers ---")123for obj in objects: #?loopobjectsoutputSwimmers: ['Fish', 'Duck'] --- Process Only Walkers ---for obj in objects: #?loopobjects
pass 1 of 4122print("\n--- Process Only Walkers ---")123for obj⟨Dog A⟩ in objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]: #?loopobjects124 if isinstance(obj, Walkable): #?checkbeforeuse125 print(f" {type(obj).__name__}: {obj.walk()}") #?callwalkAll 4 passes — pass 1 is the card above pass objself1 ⟨Dog A⟩ ⟨Dog A⟩ 2 ⟨Fish B⟩ — 3 ⟨Duck C⟩ ⟨Duck C⟩ 4 ⟨Robot D⟩ ⟨Robot D⟩ if isinstance(obj, Walkable): #?checkbeforeuse
pass 1 of 3123for obj in objects: #?loopobjects124 if isinstance(obj⟨Dog A⟩, Walkable<class '__main__.Walkable'>): #?checkbeforeuse125 print(f" {type(obj⟨Dog A⟩).__name__}: {obj.walk()}") #?callwalkAll 3 passes — pass 1 is the card above pass objself1 ⟨Dog A⟩ ⟨Dog A⟩ 2 ⟨Duck C⟩ ⟨Duck C⟩ 3 ⟨Robot D⟩ ⟨Robot D⟩ def walk(self) -> str: #?dogwalk
37def walk(self⟨Dog A⟩) -> str: #?dogwalk38 return "Dog walks on four legs"print(f" {type(obj).__name__}: {obj.walk()}") #?callwalk
124if isinstance(obj, Walkable): #?checkbeforeuse125 print(f" {type(obj⟨Dog A⟩).__name__}: {obj.walk()}") #?callwalkoutput Dog: Dog walks on four legsdef walk(self) -> str: #?duckwalk
pass 1 of 254def walk(self⟨Duck C⟩) -> str: #?duckwalk55 return "Duck waddles"print(f" {type(obj).__name__}: {obj.walk()}") #?callwalk
124if isinstance(obj, Walkable): #?checkbeforeuse125 print(f" {type(obj⟨Duck C⟩).__name__}: {obj.walk()}") #?callwalkoutput Duck: Duck waddlesdef walk(self) -> str: #?robotwalk
67def walk(self⟨Robot D⟩) -> str: #?robotwalk68 return "Robot walks mechanically"print(f" {type(obj).__name__}: {obj.walk()}") #?callwalk
124if isinstance(obj, Walkable): #?checkbeforeuse125 print(f" {type(obj⟨Robot D⟩).__name__}: {obj.walk()}") #?callwalkoutput Robot: Robot walks mechanicallyprint(" --- Process Only Swimmers ---")
127print("\n--- Process Only Swimmers ---")128for obj in objects:output --- Process Only Swimmers ---for obj in objects:
pass 1 of 4127print("\n--- Process Only Swimmers ---")128for obj⟨Dog A⟩ in objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]:129 if isinstance(obj, Swimmable):130 print(f" {type(obj).__name__}: {obj.swim()}") #?callswimAll 4 passes — pass 1 is the card above pass objSwimmableself1 ⟨Dog A⟩ — — 2 ⟨Fish B⟩ <class '__main__.Swimmable'> ⟨Fish B⟩ 3 ⟨Duck C⟩ <class '__main__.Swimmable'> ⟨Duck C⟩ 4 ⟨Robot D⟩ — — if isinstance(obj, Swimmable):
pass 1 of 2128for obj in objects:129 if isinstance(obj⟨Fish B⟩, Swimmable<class '__main__.Swimmable'>):130 print(f" {type(obj⟨Fish B⟩).__name__}: {obj.swim()}") #?callswimdef swim(self) -> str: #?fishswim
47def swim(self⟨Fish B⟩) -> str: #?fishswim48 return "Fish swims with fins"print(f" {type(obj).__name__}: {obj.swim()}") #?callswim
129if isinstance(obj, Swimmable):130 print(f" {type(obj⟨Fish B⟩).__name__}: {obj.swim()}") #?callswimoutput Fish: Fish swims with finsif isinstance(obj, Swimmable):
pass 2 of 2128for obj in objects:129 if isinstance(obj⟨Duck C⟩, Swimmable<class '__main__.Swimmable'>):130 print(f" {type(obj⟨Duck C⟩).__name__}: {obj.swim()}") #?callswimdef swim(self) -> str: #?duckswim
pass 1 of 257def swim(self⟨Duck C⟩) -> str: #?duckswim58 return "Duck paddles on water"print(f" {type(obj).__name__}: {obj.swim()}") #?callswim
129if isinstance(obj, Swimmable):130 print(f" {type(obj⟨Duck C⟩).__name__}: {obj.swim()}") #?callswimoutput Duck: Duck paddles on waterprint(" --- Finding Multi-talented (can both walk AND swim) ---")
132# Combining protocols #?combiningprotocols133print("\n--- Finding Multi-talented (can both walk AND swim) ---")134for obj in objects:output --- Finding Multi-talented (can both walk AND swim) ---for obj in objects:
pass 1 of 4133print("\n--- Finding Multi-talented (can both walk AND swim) ---")134for obj⟨Dog A⟩ in objects[⟨Dog A⟩, ⟨Fish B⟩, ⟨Duck C⟩, ⟨Robot D⟩]:135 if isinstance(obj, Walkable) and isinstance(obj, Swimmable): #?checkboth136 name = type(obj).__name__All 4 passes — pass 1 is the card above pass objWalkableSwimmableselfname1 ⟨Dog A⟩ — — — — 2 ⟨Fish B⟩ — — — — 3 ⟨Duck C⟩ <class '__main__.Walkable'> <class '__main__.Swimmable'> ⟨Duck C⟩ Duck 4 ⟨Robot D⟩ — — — — name ← Duck
134for obj in objects:135 if isinstance(obj⟨Duck C⟩, Walkable<class '__main__.Walkable'>) and isinstance(obj, Swimmable<class '__main__.Swimmable'>): #?checkboth136 name→ Duck = type(obj⟨Duck C⟩).__name__137 print(f" {nameDuck} can:")138 print(f" - {obj⟨Duck C⟩.walk()}")139 print(f" - {obj.swim()}")output Duck can:def walk(self) -> str: #?duckwalk
pass 2 of 254def walk(self⟨Duck C⟩) -> str: #?duckwalk55 return "Duck waddles"print(f" - {obj.walk()}")
137print(f" {name} can:")138print(f" - {obj⟨Duck C⟩.walk()}")139print(f" - {obj⟨Duck C⟩.swim()}")output - Duck waddlesdef swim(self) -> str: #?duckswim
pass 2 of 257def swim(self⟨Duck C⟩) -> str: #?duckswim58 return "Duck paddles on water"print(f" - {obj.swim()}")
138print(f" - {obj.walk()}")139print(f" - {obj⟨Duck C⟩.swim()}")output - Duck paddles on waterprint(" === @runtime_checkable Rules ===")
141print("\n=== @runtime_checkable Rules ===")142print("""1431. Without @runtime_checkable:144 - Protocol is for static type checking only145 - Cannot use isinstance()1461472. With @runtime_checkable:148 - Can use isinstance() at runtime149 - Checks if object has the required methods150 - Does NOT check method signatures1511523. Limitations of runtime checking:153 - Only checks method/attribute names exist154 - Does NOT verify return types155 - Does NOT verify parameter types156 - Less strict than static type checking1571584. When to use:159 - Need to filter objects by capability160 - Conditional logic based on protocol161 - Building plugin systems162""")output === @runtime_checkable Rules === 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)
""")
PI ← (empty)
6print("=== Protocol vs ABC ===\n")78# ========== ABC APPROACH ==========9print("--- ABC (Abstract Base Class) ---")1011class ShapeABC(ABC): #?shapeabc12 """13 Abstract Base Class approach.14 Subclasses MUST inherit from this.15 """16 17 @abstractmethod18 def area(self) -> float: #?abcareamethod19 """Calculate area."""20 pass21 22 @abstractmethod23 def perimeter(self) -> float: #?abcperimetermethod24 """Calculate perimeter."""25 pass262728class CircleABC(ShapeABC): #?circleabc29 """Circle MUST inherit from ShapeABC."""30 31 PI→ (empty) = 3.14159 #?abcpi32 33 def __init__(self, radius): #?abccircleinit34 self.radius = radius35 36 def area(self) -> float: #?abccirclearea37 return CircleABC.PI * self.radius ** 238 39 def perimeter(self) -> float: #?abccircleperimeter40 return 2 * CircleABC.PI * self.radius414243class RectangleABC(ShapeABC): #?rectangleabc44 """Rectangle MUST inherit from ShapeABC."""45 46 def __init__(self, width, height): #?abcrectangleinit47 self.width = width48 self.height = height49 50 def area(self) -> float: #?abcrectanglearea51 return self.width * self.height52 53 def perimeter(self) -> float: #?abcrectangleperimeter54 return 2 * (self.width + self.height)555657# ========== PROTOCOL APPROACH ==========58print("--- Protocol ---")5960@runtime_checkable #?protocolruntimecheckable61class ShapeProtocol(Protocol): #?shapeprotocol62 """63 Protocol approach.64 Classes just need matching methods.65 NO inheritance required.66 """67 68 def area(self) -> float: #?protocolareamethod69 ...70 71 def perimeter(self) -> float: #?protocolperimetermethod72 ...737475class CircleProtocol: #?circleprotocol76 """Circle WITHOUT inheritance - just has the methods."""77 78 PI→ (empty) = 3.14159 #?protocolpi79 80 def __init__(self, radius): #?protocolcircleinit81 self.radius = radius82 83 def area(self) -> float: #?protocolcirclearea84 return CircleProtocol.PI * self.radius ** 285 86 def perimeter(self) -> float: #?protocolcircleperimeter87 return 2 * CircleProtocol.PI * self.radius888990class RectangleProtocol: #?rectangleprotocol91 """Rectangle WITHOUT inheritance - just has the methods."""92 93 def __init__(self, width, height): #?protocolrectangleinit94 self.width = width95 self.height = height96 97 def area(self) -> float: #?protocolrectanglearea98 return self.width * self.height99 100 def perimeter(self) -> float: #?protocolrectangleperimeter101 return 2 * (self.width + self.height)102103104# ========== COMPARISON ==========105print("--- Comparison ---\n")106107# Function that works with ABC #?abcfunction108def calculate_abc(shape: ShapeABC) -> None: #?calculateabc109 """Takes ShapeABC - MUST be a subclass."""110 print(f" Area: {shape.area():.2f}")111 print(f" Perimeter: {shape.perimeter():.2f}")112113114# Function that works with Protocol #?protocolfunction115def calculate_protocol(shape: ShapeProtocol) -> None: #?calculateprotocol116 """Takes ShapeProtocol - just needs matching methods."""117 print(f" Area: {shape.area():.2f}")118 print(f" Perimeter: {shape.perimeter():.2f}")119120121# Create instances #?createinstances122circle_abc = CircleABC(5) #?createcircleabc123rect_abc = RectangleABC(4, 6) #?createrectabcoutput=== Protocol vs ABC === --- ABC (Abstract Base Class) --- --- Protocol --- --- Comparison ---self.radius ← 5
33def __init__(self⟨CircleABC A⟩, radius5): #?abccircleinit34 self.radius→ 5 = radius5circle_abc ← ⟨CircleABC A⟩
121# Create instances #?createinstances122circle_abc→ ⟨CircleABC A⟩ = CircleABC(5) #?createcircleabc123rect_abc = RectangleABC(4, 6) #?createrectabcself.width ← 4, self.height ← 6
46def __init__(self⟨RectangleABC B⟩, width4, height6): #?abcrectangleinit47 self.width→ 4 = width448 self.height→ 6 = height6rect_abc ← ⟨RectangleABC B⟩
122circle_abc = CircleABC(5) #?createcircleabc123rect_abc→ ⟨RectangleABC B⟩ = RectangleABC(4, 6) #?createrectabc124125circle_prot = CircleProtocol(5) #?createcircleprot126rect_prot = RectangleProtocol(4, 6) #?createrectprotself.radius ← 5
80def __init__(self⟨CircleProtocol C⟩, radius5): #?protocolcircleinit81 self.radius→ 5 = radius5circle_prot ← ⟨CircleProtocol C⟩
125circle_prot→ ⟨CircleProtocol C⟩ = CircleProtocol(5) #?createcircleprot126rect_prot = RectangleProtocol(4, 6) #?createrectprotself.width ← 4, self.height ← 6
93def __init__(self⟨RectangleProtocol D⟩, width4, height6): #?protocolrectangleinit94 self.width→ 4 = width495 self.height→ 6 = height6rect_prot ← ⟨RectangleProtocol D⟩
125circle_prot = CircleProtocol(5) #?createcircleprot126rect_prot→ ⟨RectangleProtocol D⟩ = RectangleProtocol(4, 6) #?createrectprot127128# Test ABC approach #?testabc129print("ABC Shapes:")130print("Circle:")131calculate_abc(circle_abc⟨CircleABC A⟩) #?calccircleabc132print("Rectangle:")outputABC Shapes: Circle:def calculate_abc(shape: ShapeABC) -> None: #?calculateabc
pass 1 of 2107# Function that works with ABC #?abcfunction108def calculate_abc(shape⟨CircleABC A⟩: ShapeABC) -> None: #?calculateabc109 """Takes ShapeABC - MUST be a subclass."""110 print(f" Area: {shape⟨CircleABC A⟩.area():.2f}")111 print(f" Perimeter: {shape.perimeter():.2f}")def area(self) -> float: #?abccirclearea
pass 1 of 236def area(self⟨CircleABC A⟩) -> float: #?abccirclearea37 return CircleABC.PI3.14159 * self.radius5 ** 2print(f" Area: {shape.area():.2f}")
109"""Takes ShapeABC - MUST be a subclass."""110print(f" Area: {shape⟨CircleABC A⟩.area():.2f}")111print(f" Perimeter: {shape⟨CircleABC A⟩.perimeter():.2f}")output Area: 78.54def perimeter(self) -> float: #?abccircleperimeter
pass 1 of 239def perimeter(self⟨CircleABC A⟩) -> float: #?abccircleperimeter40 return 2 * CircleABC.PI3.14159 * self.radius5print(f" Perimeter: {shape.perimeter():.2f}")
110print(f" Area: {shape.area():.2f}")111print(f" Perimeter: {shape⟨CircleABC A⟩.perimeter():.2f}")output Perimeter: 31.42calculate_abc(circle_abc) #?calccircleabc
130print("Circle:")131calculate_abc(circle_abc⟨CircleABC A⟩) #?calccircleabc132print("Rectangle:")133calculate_abc(rect_abc⟨RectangleABC B⟩) #?calcrectabcoutputRectangle:def calculate_abc(shape: ShapeABC) -> None: #?calculateabc
pass 2 of 2107# Function that works with ABC #?abcfunction108def calculate_abc(shape⟨RectangleABC B⟩: ShapeABC) -> None: #?calculateabc109 """Takes ShapeABC - MUST be a subclass."""110 print(f" Area: {shape⟨RectangleABC B⟩.area():.2f}")111 print(f" Perimeter: {shape.perimeter():.2f}")def area(self) -> float: #?abcrectanglearea
50def area(self⟨RectangleABC B⟩) -> float: #?abcrectanglearea51 return self.width4 * self.height6print(f" Area: {shape.area():.2f}")
109"""Takes ShapeABC - MUST be a subclass."""110print(f" Area: {shape⟨RectangleABC B⟩.area():.2f}")111print(f" Perimeter: {shape⟨RectangleABC B⟩.perimeter():.2f}")output Area: 24.00def perimeter(self) -> float: #?abcrectangleperimeter
53def perimeter(self⟨RectangleABC B⟩) -> float: #?abcrectangleperimeter54 return 2 * (self.width4 + self.height6)print(f" Perimeter: {shape.perimeter():.2f}")
110print(f" Area: {shape.area():.2f}")111print(f" Perimeter: {shape⟨RectangleABC B⟩.perimeter():.2f}")output Perimeter: 20.00calculate_abc(rect_abc) #?calcrectabc
132print("Rectangle:")133calculate_abc(rect_abc⟨RectangleABC B⟩) #?calcrectabc134135print()136137# Test Protocol approach #?testprotocol138print("Protocol Shapes:")139print("Circle:")140calculate_protocol(circle_prot⟨CircleProtocol C⟩) #?calccircleprot141print("Rectangle:")outputProtocol Shapes: Circle:def calculate_protocol(shape: ShapeProtocol) -> None: #?calculateproto…
pass 1 of 3114# Function that works with Protocol #?protocolfunction115def calculate_protocol(shape⟨CircleProtocol C⟩: ShapeProtocol) -> None: #?calculateprotocol116 """Takes ShapeProtocol - just needs matching methods."""117 print(f" Area: {shape⟨CircleProtocol C⟩.area():.2f}")118 print(f" Perimeter: {shape.perimeter():.2f}")All 3 passes — pass 1 is the card above pass shapeselfCircleProtocol.PIself.radiusself.widthself.heightCircleABC.PI1 ⟨CircleProtocol C⟩ ⟨CircleProtocol C⟩ 3.14159 5 — — — 2 ⟨RectangleProtocol D⟩ ⟨RectangleProtocol D⟩ — — 4 6 — 3 ⟨CircleABC A⟩ ⟨CircleABC A⟩ — 5 — — 3.14159 def area(self) -> float: #?protocolcirclearea
83def area(self⟨CircleProtocol C⟩) -> float: #?protocolcirclearea84 return CircleProtocol.PI3.14159 * self.radius5 ** 2print(f" Area: {shape.area():.2f}")
116"""Takes ShapeProtocol - just needs matching methods."""117print(f" Area: {shape⟨CircleProtocol C⟩.area():.2f}")118print(f" Perimeter: {shape⟨CircleProtocol C⟩.perimeter():.2f}")output Area: 78.54def perimeter(self) -> float: #?protocolcircleperimeter
86def perimeter(self⟨CircleProtocol C⟩) -> float: #?protocolcircleperimeter87 return 2 * CircleProtocol.PI3.14159 * self.radius5print(f" Perimeter: {shape.perimeter():.2f}")
117print(f" Area: {shape.area():.2f}")118print(f" Perimeter: {shape⟨CircleProtocol C⟩.perimeter():.2f}")output Perimeter: 31.42calculate_protocol(circle_prot) #?calccircleprot
139print("Circle:")140calculate_protocol(circle_prot⟨CircleProtocol C⟩) #?calccircleprot141print("Rectangle:")142calculate_protocol(rect_prot⟨RectangleProtocol D⟩) #?calcrectprotoutputRectangle:def area(self) -> float: #?protocolrectanglearea
97def area(self⟨RectangleProtocol D⟩) -> float: #?protocolrectanglearea98 return self.width4 * self.height6print(f" Area: {shape.area():.2f}")
116"""Takes ShapeProtocol - just needs matching methods."""117print(f" Area: {shape⟨RectangleProtocol D⟩.area():.2f}")118print(f" Perimeter: {shape⟨RectangleProtocol D⟩.perimeter():.2f}")output Area: 24.00def perimeter(self) -> float: #?protocolrectangleperimeter
100def perimeter(self⟨RectangleProtocol D⟩) -> float: #?protocolrectangleperimeter101 return 2 * (self.width4 + self.height6)print(f" Perimeter: {shape.perimeter():.2f}")
117print(f" Area: {shape.area():.2f}")118print(f" Perimeter: {shape⟨RectangleProtocol D⟩.perimeter():.2f}")output Perimeter: 20.00calculate_protocol(rect_prot) #?calcrectprot
141print("Rectangle:")142calculate_protocol(rect_prot⟨RectangleProtocol D⟩) #?calcrectprot143144print()145146# Key difference: inheritance check #?inheritancecheck147print("--- Inheritance Check ---")148print(f"CircleABC is subclass of ShapeABC: {issubclass(CircleABC<class '__main__.CircleABC'>, ShapeABC<class '__main__.ShapeABC'>)}") #?abcsubclass149print(f"CircleProtocol is subclass of ShapeProtocol: {isinstance(circle_prot⟨CircleProtocol C⟩, ShapeProtocol<class '__main__.ShapeProtocol'>)}") #?protocolinstance150151# ABC enforces at class definition #?abcenforcement152print("\n--- ABC Enforcement ---")153154class IncompleteABC(ShapeABC): #?incompleteabc155 """ABC enforces implementation at instantiation."""output--- Inheritance Check --- CircleABC is subclass of ShapeABC: True CircleProtocol is subclass of ShapeProtocol: True --- ABC Enforcement ---except TypeError as e: #?abctypeerror
162 incomplete = IncompleteABC() #?tryincomplete163except TypeError as e: #?abctypeerror164 print(f"ABC Error: {eCan't instantiate abstract class IncompleteABC without an implementation for abstract method 'perimeter'}")outputABC Error: Can't instantiate abstract class IncompleteABC without an implementation for abstract method 'perimeter'incomplete_prot ← ⟨IncompleteProtocol E⟩
166# Protocol allows incomplete (type checker catches it) #?protocolincomplete167print("\n--- Protocol (no enforcement at runtime) ---")168169class IncompleteProtocol: #?incompleteprotocol170 """Protocol doesn't enforce at runtime."""171 172 def area(self) -> float: #?incompleteprotarea173 return 0174 # Missing perimeter() - but no error at creation #?noerror175176incomplete_prot→ ⟨IncompleteProtocol E⟩ = IncompleteProtocol() #?createincomplete177print(f"IncompleteProtocol created successfully")178print(f"isinstance check: {isinstance(incomplete_prot⟨IncompleteProtocol E⟩, ShapeProtocol<class '__main__.ShapeProtocol'>)}") #?failedisinstance179180# Cross-compatibility demonstration #?crosscompat181print("\n--- Cross-Compatibility ---")182print("Protocol function can accept ABC classes:")183calculate_protocol(circle_abc⟨CircleABC A⟩) #?abcinprotocol184print("(ABC classes satisfy Protocol if they have the methods!)")output --- Protocol (no enforcement at runtime) --- IncompleteProtocol created successfully isinstance check: False --- Cross-Compatibility --- Protocol function can accept ABC classes:def area(self) -> float: #?abccirclearea
pass 2 of 236def area(self⟨CircleABC A⟩) -> float: #?abccirclearea37 return CircleABC.PI3.14159 * self.radius5 ** 2print(f" Area: {shape.area():.2f}")
116"""Takes ShapeProtocol - just needs matching methods."""117print(f" Area: {shape⟨CircleABC A⟩.area():.2f}")118print(f" Perimeter: {shape⟨CircleABC A⟩.perimeter():.2f}")output Area: 78.54def perimeter(self) -> float: #?abccircleperimeter
pass 2 of 239def perimeter(self⟨CircleABC A⟩) -> float: #?abccircleperimeter40 return 2 * CircleABC.PI3.14159 * self.radius5print(f" Perimeter: {shape.perimeter():.2f}")
117print(f" Area: {shape.area():.2f}")118print(f" Perimeter: {shape⟨CircleABC A⟩.perimeter():.2f}")output Perimeter: 31.42calculate_protocol(circle_abc) #?abcinprotocol
182print("Protocol function can accept ABC classes:")183calculate_protocol(circle_abc⟨CircleABC A⟩) #?abcinprotocol184print("(ABC classes satisfy Protocol if they have the methods!)")185186print("\n=== When to Use Which? ===")187print("""188Use ABC when:189├─ You need runtime enforcement190├─ You want to provide default implementations191├─ You have a clear inheritance hierarchy192├─ Classes MUST explicitly inherit193└─ Example: Framework base classes, plugin systems194195Use Protocol when:196├─ You want structural (duck) typing197├─ Classes from different libraries should work198├─ No inheritance relationship desired199├─ Type hints for existing code without changes200└─ Example: Accepting any "file-like" object201202Can use both:203├─ Protocol for type hints (flexible)204├─ ABC for your own implementations (enforced)205""")output(ABC classes satisfy Protocol if they have the methods!) === When to Use Which? === 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):
self.start = 1000.0
print(f"[{self.name}] Starting...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = 1000.001 - 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]
""")
print("=== Common Protocol Patterns === ")
5print("=== Common Protocol Patterns ===\n")67# ========== ITERABLE PATTERN ==========8print("--- Iterable Pattern ---")910@runtime_checkable11class IterableProtocol(Protocol): #?iterableprotocol12 """Protocol for objects that can be iterated."""13 14 def __iter__(self): #?itermethod15 ...161718class NumberRange: #?numberrangeclass19 """Custom iterable - numbers from start to end."""20 21 def __init__(self, start, end): #?rangeinit22 self.start = start23 self.end = end24 25 def __iter__(self): #?rangeiter26 current = self.start27 while current <= self.end: #?rangeloop28 yield current #?yieldcurrent29 current += 1303132# Use in for loop #?iterableuse33numbers = NumberRange(1, 5) #?createnumbers34print(f"NumberRange(1, 5): {list(numbers)}") #?printnumbersoutput=== Common Protocol Patterns === --- Iterable Pattern ---self.start ← 1, self.end ← 5
21def __init__(self⟨NumberRange A⟩, start1, end5): #?rangeinit22 self.start→ 1 = start123 self.end→ 5 = end5numbers ← ⟨NumberRange A⟩
32# Use in for loop #?iterableuse33numbers→ ⟨NumberRange A⟩ = NumberRange(1, 5) #?createnumbers34print(f"NumberRange(1, 5): {list(numbers⟨NumberRange A⟩)}") #?printnumbers35print(f"Is Iterable: {isinstance(numbers, IterableProtocol)}") #?checkiterablecurrent ← 1
25def __iter__(self⟨NumberRange A⟩): #?rangeiter26 current→ 1 = self.start127 while current <= self.end: #?rangeloopcurrent ← 2
pass 1 of 526current = self.start27while current1 <= self.end5: #?rangeloop28 yield current1 #?yieldcurrent29 current→ 2 += 1All 5 passes — pass 1 is the card above pass current1 1 → 2 2 2 → 3 3 3 → 4 4 4 → 5 5 5 → 6 print(f"NumberRange(1, 5): {list(numbers)}") #?printnumbers
33numbers = NumberRange(1, 5) #?createnumbers34print(f"NumberRange(1, 5): {list(numbers⟨NumberRange A⟩)}") #?printnumbers35print(f"Is Iterable: {isinstance(numbers⟨NumberRange A⟩, IterableProtocol<class '__main__.IterableProtocol'>)}") #?checkiterable3637print()3839# ========== CALLABLE PATTERN ==========40print("--- Callable Pattern ---")4142@runtime_checkable43class CallableProtocol(Protocol): #?callableprotocol44 """Protocol for objects that can be called like functions."""45 46 def __call__(self, *args, **kwargs): #?callmethod47 ...484950class Multiplier: #?multiplierclass51 """Callable class - multiplies by a factor."""52 53 def __init__(self, factor): #?multiplierinit54 self.factor = factor55 56 def __call__(self, value): #?multipliercall57 return value * self.factor585960double = Multiplier(2) #?createdouble61triple = Multiplier(3) #?createtripleoutputNumberRange(1, 5): [1, 2, 3, 4, 5] Is Iterable: True --- Callable Pattern ---self.factor ← 2
pass 1 of 253def __init__(self⟨Multiplier B⟩, factor2): #?multiplierinit54 self.factor→ 2 = factor2double ← ⟨Multiplier B⟩
60double→ ⟨Multiplier B⟩ = Multiplier(2) #?createdouble61triple = Multiplier(3) #?createtripleself.factor ← 3
pass 2 of 253def __init__(self⟨Multiplier C⟩, factor3): #?multiplierinit54 self.factor→ 3 = factor3triple ← ⟨Multiplier C⟩
60double = Multiplier(2) #?createdouble61triple→ ⟨Multiplier C⟩ = Multiplier(3) #?createtriple6263print(f"double(5) = {double(5)}") #?calldouble64print(f"triple(5) = {triple(5)}") #?calltripledef __call__(self, value): #?multipliercall
pass 1 of 256def __call__(self⟨Multiplier B⟩, value5): #?multipliercall57 return value5 * self.factor2print(f"double(5) = {double(5)}") #?calldouble
63print(f"double(5) = {double(5)}") #?calldouble64print(f"triple(5) = {triple(5)}") #?calltriple65print(f"Is Callable: {isinstance(double, CallableProtocol)}") #?checkcallableoutputdouble(5) = 10def __call__(self, value): #?multipliercall
pass 2 of 256def __call__(self⟨Multiplier C⟩, value5): #?multipliercall57 return value5 * self.factor3print(f"Is Callable: {isinstance(double, CallableProtocol)}") #?checkc…
63print(f"double(5) = {double(5)}") #?calldouble64print(f"triple(5) = {triple(5)}") #?calltriple65print(f"Is Callable: {isinstance(double⟨Multiplier B⟩, CallableProtocol<class '__main__.CallableProtocol'>)}") #?checkcallable6667# Regular functions are also callable #?funcallable68def square(x): #?squarefunction69 return x * x7071print(f"square is Callable: {isinstance(square⟨function square D⟩, CallableProtocol<class '__main__.CallableProtocol'>)}") #?checksquare7273print()7475# ========== SIZED PATTERN ==========76print("--- Sized Pattern ---")7778@runtime_checkable79class SizedProtocol(Protocol): #?sizedprotocol80 """Protocol for objects with length."""81 82 def __len__(self) -> int: #?lenmethod83 ...848586class Playlist: #?playlistclass87 """Sized class - has length."""88 89 def __init__(self, name): #?playlistinit90 self.name = name91 self.songs = [] #?playlistsongs92 93 def add(self, song): #?playlistadd94 self.songs.append(song)95 96 def __len__(self) -> int: #?playlistlen97 return len(self.songs)9899100playlist = Playlist("My Mix") #?createplaylist101playlist.add("Song A") #?addsong1outputtriple(5) = 15 Is Callable: True square is Callable: True --- Sized Pattern ---self.name ← My Mix, self.songs ← []
89def __init__(self⟨Playlist E⟩, nameMy Mix): #?playlistinit90 self.name→ My Mix = nameMy Mix91 self.songs→ [] = [] #?playlistsongsplaylist ← ⟨Playlist E⟩
100playlist→ ⟨Playlist E⟩ = Playlist("My Mix") #?createplaylist101playlist⟨Playlist E⟩.add("Song A") #?addsong1102playlist.add("Song B") #?addsong2self.songs ← ['Song A']
pass 1 of 393def add(self⟨Playlist E⟩, songSong A): #?playlistadd94 self.songs→ ['Song A'].append(songSong A)All 3 passes — pass 1 is the card above pass songself.songs1 Song A [] → ['Song A'] 2 Song B ['Song A'] → ['Song A', 'Song B'] 3 Song C ['Song A', 'Song B'] → ['Song A', 'Song B', 'Song C'] playlist.add("Song A") #?addsong1
100playlist = Playlist("My Mix") #?createplaylist101playlist⟨Playlist E⟩.add("Song A") #?addsong1102playlist⟨Playlist E⟩.add("Song B") #?addsong2103playlist.add("Song C") #?addsong3playlist.add("Song B") #?addsong2
101playlist.add("Song A") #?addsong1102playlist⟨Playlist E⟩.add("Song B") #?addsong2103playlist⟨Playlist E⟩.add("Song C") #?addsong3playlist.add("Song C") #?addsong3
102playlist.add("Song B") #?addsong2103playlist⟨Playlist E⟩.add("Song C") #?addsong3104105print(f"Playlist '{playlist.nameMy Mix}' has {len(playlist⟨Playlist E⟩)} songs") #?printplaylist106print(f"Is Sized: {isinstance(playlist, SizedProtocol)}") #?checksizeddef __len__(self) -> int: #?playlistlen
96def __len__(self⟨Playlist E⟩) -> int: #?playlistlen97 return len(self.songs['Song A', 'Song B', 'Song C'])print(f"Playlist '{playlist.name}' has {len(playlist)} songs") #?print…
105print(f"Playlist '{playlist.nameMy Mix}' has {len(playlist⟨Playlist E⟩)} songs") #?printplaylist106print(f"Is Sized: {isinstance(playlist⟨Playlist E⟩, SizedProtocol<class '__main__.SizedProtocol'>)}") #?checksized107108print()109110# ========== CONTAINER PATTERN ==========111print("--- Container Pattern (supports 'in') ---")112113@runtime_checkable114class ContainerProtocol(Protocol): #?containerprotocol115 """Protocol for objects that support 'in' operator."""116 117 def __contains__(self, item) -> bool: #?containsmethod118 ...119120121class ShoppingCart: #?shoppingcartclass122 """Container class - supports 'in' operator."""123 124 def __init__(self): #?cartinit125 self.items = [] #?cartitems126 127 def add(self, item): #?cartadd128 self.items.append(item)129 130 def __contains__(self, item) -> bool: #?cartcontains131 return item in self.items132133134cart = ShoppingCart() #?createcart135cart.add("Apple") #?addappleoutputPlaylist 'My Mix' has 3 songs Is Sized: True --- Container Pattern (supports 'in') ---self.items ← []
124def __init__(self⟨ShoppingCart F⟩): #?cartinit125 self.items→ [] = [] #?cartitemscart ← ⟨ShoppingCart F⟩
134cart→ ⟨ShoppingCart F⟩ = ShoppingCart() #?createcart135cart⟨ShoppingCart F⟩.add("Apple") #?addapple136cart.add("Banana") #?addbananaself.items ← ['Apple']
pass 1 of 2127def add(self⟨ShoppingCart F⟩, itemApple): #?cartadd128 self.items→ ['Apple'].append(itemApple)cart.add("Apple") #?addapple
134cart = ShoppingCart() #?createcart135cart⟨ShoppingCart F⟩.add("Apple") #?addapple136cart⟨ShoppingCart F⟩.add("Banana") #?addbananaself.items ← ['Apple', 'Banana']
pass 2 of 2127def add(self⟨ShoppingCart F⟩, itemBanana): #?cartadd128 self.items→ ['Apple', 'Banana'].append(itemBanana)cart.add("Banana") #?addbanana
135cart.add("Apple") #?addapple136cart⟨ShoppingCart F⟩.add("Banana") #?addbanana137138print(f"'Apple' in cart: {'Apple' in cart⟨ShoppingCart F⟩}") #?incheckApple139print(f"'Orange' in cart: {'Orange' in cart}") #?incheckorangedef __contains__(self, item) -> bool: #?cartcontains
pass 1 of 2130def __contains__(self⟨ShoppingCart F⟩, itemApple) -> bool: #?cartcontains131 return itemApple in self.items['Apple', 'Banana']print(f"'Apple' in cart: {'Apple' in cart}") #?incheckApple
138print(f"'Apple' in cart: {'Apple' in cart⟨ShoppingCart F⟩}") #?incheckApple139print(f"'Orange' in cart: {'Orange' in cart⟨ShoppingCart F⟩}") #?incheckorange140print(f"Is Container: {isinstance(cart, ContainerProtocol)}") #?checkcontaineroutput'Apple' in cart: Truedef __contains__(self, item) -> bool: #?cartcontains
pass 2 of 2130def __contains__(self⟨ShoppingCart F⟩, itemOrange) -> bool: #?cartcontains131 return itemOrange in self.items['Apple', 'Banana']print(f"'Orange' in cart: {'Orange' in cart}") #?incheckorange
138print(f"'Apple' in cart: {'Apple' in cart}") #?incheckApple139print(f"'Orange' in cart: {'Orange' in cart⟨ShoppingCart F⟩}") #?incheckorange140print(f"Is Container: {isinstance(cart⟨ShoppingCart F⟩, ContainerProtocol<class '__main__.ContainerProtocol'>)}") #?checkcontainer141142print()143144# ========== CONTEXT MANAGER PATTERN ==========145print("--- Context Manager Pattern ---")146147@runtime_checkable148class ContextManagerProtocol(Protocol): #?contextmanagerprotocol149 """Protocol for objects that work with 'with' statement."""150 151 def __enter__(self): #?entermethod152 ...153 154 def __exit__(self, exc_type, exc_val, exc_tb): #?exitmethod155 ...156157158class Timer: #?timerclass159 """Context manager that measures execution time."""160 161 def __init__(self, name): #?timerinit162 self.name = name163 self.start = 0 #?timerstart164 165 def __enter__(self): #?timerenter166 self.start = 1000.0167 print(f"[{self.name}] Starting...") #?printstart168 return self #?returnself169 170 def __exit__(self, exc_type, exc_val, exc_tb): #?timerexit171 elapsed = 1000.001 - self.start #?calculateelapsed172 print(f"[{self.name}] Completed in {elapsed:.4f} seconds")173 return False #?returnfalse174175176print(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerProtocol<class '__main__.ContextManagerProtocol'>)}") #?checkcontextoutput'Orange' in cart: False Is Container: True --- Context Manager Pattern ---self.name ← test, self.start ← 0
pass 1 of 2161def __init__(self⟨Timer G⟩, nametest): #?timerinit162 self.name→ test = nametest163 self.start→ 0 = 0 #?timerstartprint(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerP…
176print(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerProtocol<class '__main__.ContextManagerProtocol'>)}") #?checkcontextoutputIs Context Manager: Trueself.name ← Processing, self.start ← 0
pass 2 of 2161def __init__(self⟨Timer G⟩, nameProcessing): #?timerinit162 self.name→ Processing = nameProcessing163 self.start→ 0 = 0 #?timerstartself.start ← 1000.0
165def __enter__(self⟨Timer G⟩): #?timerenter166 self.start→ 1000.0 = 1000.0167 print(f"[{self.nameProcessing}] Starting...") #?printstart168 return self #?returnselfoutput[Processing] Starting...total ← 49995000
178# Use the context manager #?usecontextmanager179with Timer("Processing") as t: #?withstatement180 total→ 49995000 = sum(range(10000)) #?somework181 print(f" Sum calculated: {total49995000}")output Sum calculated: 49995000elapsed ← 0.0009999999999763531
170def __exit__(self⟨Timer G⟩, exc_typeNone, exc_valNone, exc_tbNone): #?timerexit171 elapsed→ 0.0009999999999763531 = 1000.001 - self.start1000.0 #?calculateelapsed172 print(f"[{self.nameProcessing}] Completed in {elapsed0.0009999999999763531:.4f} seconds")173 return False #?returnfalseoutput[Processing] Completed in 0.0010 secondsprint()
183print()184185# ========== COMBINING PROTOCOLS ==========186print("--- Combining Multiple Protocols ---")187188class DataStore: #?datastoreclass189 """Class implementing multiple protocols."""190 191 def __init__(self): #?datastoreinit192 self.data = []193 194 def add(self, item): #?datastoreadd195 self.data.append(item)196 197 # Iterable #?iterableimpl198 def __iter__(self): #?datastoreiter199 return iter(self.data)200 201 # Sized #?sizedimpl202 def __len__(self) -> int: #?datastorelen203 return len(self.data)204 205 # Container #?containerimpl206 def __contains__(self, item) -> bool: #?datastorecontains207 return item in self.data208209210store = DataStore() #?createstore211store.add("A") #?storeaddAoutput--- Combining Multiple Protocols ---self.data ← []
191def __init__(self⟨DataStore H⟩): #?datastoreinit192 self.data→ [] = []store ← ⟨DataStore H⟩
210store→ ⟨DataStore H⟩ = DataStore() #?createstore211store⟨DataStore H⟩.add("A") #?storeaddA212store.add("B") #?storeaddBself.data ← ['A']
pass 1 of 3194def add(self⟨DataStore H⟩, itemA): #?datastoreadd195 self.data→ ['A'].append(itemA)All 3 passes — pass 1 is the card above pass itemself.data1 A [] → ['A'] 2 B ['A'] → ['A', 'B'] 3 C ['A', 'B'] → ['A', 'B', 'C'] store.add("A") #?storeaddA
210store = DataStore() #?createstore211store⟨DataStore H⟩.add("A") #?storeaddA212store⟨DataStore H⟩.add("B") #?storeaddB213store.add("C") #?storeaddCstore.add("B") #?storeaddB
211store.add("A") #?storeaddA212store⟨DataStore H⟩.add("B") #?storeaddB213store⟨DataStore H⟩.add("C") #?storeaddCstore.add("C") #?storeaddC
212store.add("B") #?storeaddB213store⟨DataStore H⟩.add("C") #?storeaddC214215print(f"DataStore: {list(store⟨DataStore H⟩)}") #?liststore216print(f"Length: {len(store)}") #?lenstoredef __iter__(self): #?datastoreiter
197# Iterable #?iterableimpl198def __iter__(self⟨DataStore H⟩): #?datastoreiter199 return iter(self.data['A', 'B', 'C'])def __len__(self) -> int: #?datastorelen
pass 1 of 2201# Sized #?sizedimpl202def __len__(self⟨DataStore H⟩) -> int: #?datastorelen203 return len(self.data['A', 'B', 'C'])print(f"DataStore: {list(store)}") #?liststore
215print(f"DataStore: {list(store⟨DataStore H⟩)}") #?liststore216print(f"Length: {len(store⟨DataStore H⟩)}") #?lenstore217print(f"'B' in store: {'B' in store}") #?instoreoutputDataStore: ['A', 'B', 'C']def __len__(self) -> int: #?datastorelen
pass 2 of 2201# Sized #?sizedimpl202def __len__(self⟨DataStore H⟩) -> int: #?datastorelen203 return len(self.data['A', 'B', 'C'])print(f"Length: {len(store)}") #?lenstore
215print(f"DataStore: {list(store)}") #?liststore216print(f"Length: {len(store⟨DataStore H⟩)}") #?lenstore217print(f"'B' in store: {'B' in store⟨DataStore H⟩}") #?instoreoutputLength: 3def __contains__(self, item) -> bool: #?datastorecontains
205# Container #?containerimpl206def __contains__(self⟨DataStore H⟩, itemB) -> bool: #?datastorecontains207 return itemB in self.data['A', 'B', 'C']print(f"'B' in store: {'B' in store}") #?instore
216print(f"Length: {len(store)}") #?lenstore217print(f"'B' in store: {'B' in store⟨DataStore H⟩}") #?instore218219print("\nProtocol checks:")220print(f" Is Iterable: {isinstance(store⟨DataStore H⟩, IterableProtocol<class '__main__.IterableProtocol'>)}") #?checkstoreiterable221print(f" Is Sized: {isinstance(store⟨DataStore H⟩, SizedProtocol<class '__main__.SizedProtocol'>)}") #?checkstoresized222print(f" Is Container: {isinstance(store⟨DataStore H⟩, ContainerProtocol<class '__main__.ContainerProtocol'>)}") #?checkstorecontainer223224print("\n=== Standard Library Protocols ===")225print("""226typing module provides many protocols:227228Iterable[T] - has __iter__()229Iterator[T] - has __iter__() and __next__()230Callable[...,R] - can be called231Sized - has __len__()232Container[T] - has __contains__()233Hashable - has __hash__()234Reversible[T] - has __reversed__()235SupportsInt - has __int__()236SupportsFloat - has __float__()237SupportsAbs - has __abs__()238SupportsBytes - has __bytes__()239240Example usage:241 from typing import Iterable, Callable242 243 def process(items: Iterable[str], func: Callable[[str], str]):244 return [func(item) for item in items]245""")output'B' in store: True Protocol checks: Is Iterable: True Is Sized: True Is Container: True === Standard Library Protocols === 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