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.

shapes
basic_protocol.py
Replay: real traced execution (multi-file project)
# 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!
""")

  1. 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) #?createrectangle
    output=== Basic Protocols ===
  2. self.radius ← 5

    24def __init__(self⟨Circle A⟩, radius5): #?circleinit25    self.radius→ 5 = radius5
  3. circle ← ⟨Circle A⟩

    81# Create objects (none inherit from Drawable!) #?createobjects82circle→ ⟨Circle A⟩ = Circle(5) #?createcircle83rectangle = Rectangle(10, 20) #?createrectangle84text = Text("Hello") #?createtext
  4. self.width ← 10, self.height ← 20

    34def __init__(self⟨Rectangle B⟩, width10, height20): #?rectangleinit35    self.width→ 10 = width1036    self.height→ 20 = height20
  5. rectangle ← ⟨Rectangle B⟩

    82circle = Circle(5) #?createcircle83rectangle→ ⟨Rectangle B⟩ = Rectangle(10, 20) #?createrectangle84text = Text("Hello") #?createtext85line = Line((0, 0), (10, 10)) #?createline
  6. self.content ← Hello

    45def __init__(self⟨Text C⟩, contentHello): #?textinit46    self.content→ Hello = contentHello
  7. text ← ⟨Text C⟩

    83rectangle = Rectangle(10, 20) #?createrectangle84text→ ⟨Text C⟩ = Text("Hello") #?createtext85line = Line((0, 0), (10, 10)) #?createline
  8. self.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)
  9. 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) #?renderrectangle
    output--- Individual Rendering ---
  10. def render_shape(shape: Drawable) -> None: #?rendershape

    pass 1 of 8
    63# 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()}") #?callsdraw
    All 8 passes — pass 1 is the card above
    passshapeselfself.radiusself.widthself.heightself.contentself.startself.end
    1⟨Circle A⟩⟨Circle A⟩5
    2⟨Rectangle B⟩⟨Rectangle B⟩1020
    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⟩1020
    7⟨Text C⟩⟨Text C⟩Hello
    8⟨Line D⟩⟨Line D⟩(0, 0)(10, 10)
  11. def draw(self) -> str: #?circledraw

    pass 1 of 2
    27def draw(self⟨Circle A⟩) -> str: #?circledraw28    return f"Drawing circle with radius {self.radius5}"
  12. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}") #?callsdraw
    outputRendering: Drawing circle with radius 5
  13. render_shape(circle) #?rendercircle

    88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩) #?rendercircle90render_shape(rectangle⟨Rectangle B⟩) #?renderrectangle91render_shape(text) #?rendertext
  14. def draw(self) -> str: #?rectangledraw

    pass 1 of 2
    38def draw(self⟨Rectangle B⟩) -> str: #?rectangledraw39    return f"Drawing rectangle {self.width10}x{self.height20}"
  15. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}") #?callsdraw
    outputRendering: Drawing rectangle 10x20
  16. render_shape(rectangle) #?renderrectangle

    89render_shape(circle) #?rendercircle90render_shape(rectangle⟨Rectangle B⟩) #?renderrectangle91render_shape(text⟨Text C⟩) #?rendertext92render_shape(line) #?renderline
  17. def draw(self) -> str: #?textdraw

    pass 1 of 2
    48def draw(self⟨Text C⟩) -> str: #?textdraw49    return f"Drawing text: '{self.contentHello}'"
  18. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}") #?callsdraw
    outputRendering: Drawing text: 'Hello'
  19. render_shape(text) #?rendertext

    90render_shape(rectangle) #?renderrectangle91render_shape(text⟨Text C⟩) #?rendertext92render_shape(line⟨Line D⟩) #?renderline
  20. def draw(self) -> str: #?linedraw

    pass 1 of 2
    59def draw(self⟨Line D⟩) -> str: #?linedraw60    return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"
  21. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}") #?callsdraw
    outputRendering: Drawing line from (0, 0) to (10, 10)
  22. 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⟩]) #?callrenderall
  23. def 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: #?iterateshapes
    output=== Rendering All Shapes ===
  24. for shape in shapes: #?iterateshapes

    pass 1 of 4
    74print("=== Rendering All Shapes ===")75for shape⟨Circle A⟩ in shapes[⟨Circle A⟩, ⟨Rectangle B⟩, ⟨Text C⟩, ⟨Line D⟩]: #?iterateshapes76    render_shape(shape⟨Circle A⟩) #?rendereachshape
    All 4 passes — pass 1 is the card above
    passshapeselfself.radiusself.widthself.heightself.contentself.startself.end
    1⟨Circle A⟩⟨Circle A⟩5
    2⟨Rectangle B⟩⟨Rectangle B⟩1020
    3⟨Text C⟩⟨Text C⟩Hello
    4⟨Line D⟩⟨Line D⟩(0, 0)(10, 10)
  25. def draw(self) -> str: #?circledraw

    pass 2 of 2
    27def draw(self⟨Circle A⟩) -> str: #?circledraw28    return f"Drawing circle with radius {self.radius5}"
  26. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}") #?callsdraw
    outputRendering: Drawing circle with radius 5
  27. render_shape(shape) #?rendereachshape

    75for shape in shapes: #?iterateshapes76    render_shape(shape⟨Circle A⟩) #?rendereachshape
  28. def draw(self) -> str: #?rectangledraw

    pass 2 of 2
    38def draw(self⟨Rectangle B⟩) -> str: #?rectangledraw39    return f"Drawing rectangle {self.width10}x{self.height20}"
  29. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}") #?callsdraw
    outputRendering: Drawing rectangle 10x20
  30. render_shape(shape) #?rendereachshape

    75for shape in shapes: #?iterateshapes76    render_shape(shape⟨Rectangle B⟩) #?rendereachshape
  31. def draw(self) -> str: #?textdraw

    pass 2 of 2
    48def draw(self⟨Text C⟩) -> str: #?textdraw49    return f"Drawing text: '{self.contentHello}'"
  32. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}") #?callsdraw
    outputRendering: Drawing text: 'Hello'
  33. render_shape(shape) #?rendereachshape

    75for shape in shapes: #?iterateshapes76    render_shape(shape⟨Text C⟩) #?rendereachshape
  34. def draw(self) -> str: #?linedraw

    pass 2 of 2
    59def draw(self⟨Line D⟩) -> str: #?linedraw60    return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"
  35. print(f"Rendering: {shape.draw()}") #?callsdraw

    68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}") #?callsdraw
    outputRendering: Drawing line from (0, 0) to (10, 10)
  36. render_shape(shape) #?rendereachshape

    75for shape in shapes: #?iterateshapes76    render_shape(shape⟨Line D⟩) #?rendereachshape
  37. render_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) #?createpoint
    output
    --- Non-Drawable Object ---
  38. self.x ← 5, self.y ← 10

    107def __init__(self⟨Point E⟩, x5, y10): #?pointinit108    self.x→ 5 = x5109    self.y→ 10 = y10
  39. point ← ⟨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!
  1. 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 ===
  2. self.radius ← 5

    24def __init__(self⟨Circle A⟩, radius5):25    self.radius→ 5 = radius5
  3. circle ← ⟨Circle A⟩

    81# Create objects (none inherit from Drawable!)82circle→ ⟨Circle A⟩ = Circle(5)83rectangle = Rectangle(10, 20)84text = Text("Hello")
  4. self.width ← 10, self.height ← 20

    34def __init__(self⟨Rectangle B⟩, width10, height20):35    self.width→ 10 = width1036    self.height→ 20 = height20
  5. rectangle ← ⟨Rectangle B⟩

    82circle = Circle(5)83rectangle→ ⟨Rectangle B⟩ = Rectangle(10, 20)84text = Text("Hello")85line = Line((0, 0), (10, 10))
  6. self.content ← Hello

    45def __init__(self⟨Text C⟩, contentHello):46    self.content→ Hello = contentHello
  7. text ← ⟨Text C⟩

    83rectangle = Rectangle(10, 20)84text→ ⟨Text C⟩ = Text("Hello")85line = Line((0, 0), (10, 10))
  8. 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)
  9. 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 ---
  10. def render_shape(shape: Drawable) -> None:

    pass 1 of 6
    63# 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
    passshapeselfself.radiusself.widthself.heightself.contentself.startself.end
    1⟨Circle A⟩⟨Circle A⟩5
    2⟨Rectangle B⟩⟨Rectangle B⟩1020
    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
  11. def draw(self) -> str:

    pass 1 of 2
    27def draw(self⟨Circle A⟩) -> str:28    return f"Drawing circle with radius {self.radius5}"
  12. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}")
    outputRendering: Drawing circle with radius 5
  13. render_shape(circle)

    88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text)
  14. def draw(self) -> str:

    38def draw(self⟨Rectangle B⟩) -> str:39    return f"Drawing rectangle {self.width10}x{self.height20}"
  15. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}")
    outputRendering: Drawing rectangle 10x20
  16. render_shape(rectangle)

    89render_shape(circle)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text⟨Text C⟩)92render_shape(line)
  17. def draw(self) -> str:

    pass 1 of 2
    48def draw(self⟨Text C⟩) -> str:49    return f"Drawing text: '{self.contentHello}'"
  18. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}")
    outputRendering: Drawing text: 'Hello'
  19. render_shape(text)

    90render_shape(rectangle)91render_shape(text⟨Text C⟩)92render_shape(line⟨Line D⟩)
  20. 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)}"
  21. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}")
    outputRendering: Drawing line from (0, 0) to (10, 10)
  22. 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⟩])
  23. 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 ===
  24. for shape in shapes:

    pass 1 of 2
    74print("=== Rendering All Shapes ===")75for shape⟨Circle A⟩ in shapes[⟨Circle A⟩, ⟨Text C⟩]:76    render_shape(shape⟨Circle A⟩)
  25. def draw(self) -> str:

    pass 2 of 2
    27def draw(self⟨Circle A⟩) -> str:28    return f"Drawing circle with radius {self.radius5}"
  26. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}")
    outputRendering: Drawing circle with radius 5
  27. render_shape(shape)

    75for shape in shapes:76    render_shape(shape⟨Circle A⟩)
  28. for shape in shapes:

    pass 2 of 2
    74print("=== Rendering All Shapes ===")75for shape⟨Text C⟩ in shapes[⟨Circle A⟩, ⟨Text C⟩]:76    render_shape(shape⟨Text C⟩)
  29. def draw(self) -> str:

    pass 2 of 2
    48def draw(self⟨Text C⟩) -> str:49    return f"Drawing text: '{self.contentHello}'"
  30. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}")
    outputRendering: Drawing text: 'Hello'
  31. render_shape(shape)

    75for shape in shapes:76    render_shape(shape⟨Text C⟩)
  32. 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 ---
  33. self.x ← 5, self.y ← 10

    106def __init__(self⟨Point E⟩, x5, y10):107    self.x→ 5 = x5108    self.y→ 10 = y10
  34. point ← ⟨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!
  1. 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 ===
  2. self.radius ← 5

    24def __init__(self⟨Circle A⟩, radius5):25    self.radius→ 5 = radius5
  3. circle ← ⟨Circle A⟩

    81# Create objects (none inherit from Drawable!)82circle→ ⟨Circle A⟩ = Circle(5)83rectangle = Rectangle(10, 20)84text = Text("Hello")
  4. self.width ← 10, self.height ← 20

    34def __init__(self⟨Rectangle B⟩, width10, height20):35    self.width→ 10 = width1036    self.height→ 20 = height20
  5. rectangle ← ⟨Rectangle B⟩

    82circle = Circle(5)83rectangle→ ⟨Rectangle B⟩ = Rectangle(10, 20)84text = Text("Hello")85line = Line((0, 0), (10, 10))
  6. self.content ← Hello

    45def __init__(self⟨Text C⟩, contentHello):46    self.content→ Hello = contentHello
  7. text ← ⟨Text C⟩

    83rectangle = Rectangle(10, 20)84text→ ⟨Text C⟩ = Text("Hello")85line = Line((0, 0), (10, 10))
  8. 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)
  9. 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 ---
  10. def render_shape(shape: Drawable) -> None:

    pass 1 of 6
    63# 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
    passshapeselfself.radiusself.widthself.heightself.contentself.startself.end
    1⟨Circle A⟩⟨Circle A⟩5
    2⟨Rectangle B⟩⟨Rectangle B⟩1020
    3⟨Text C⟩⟨Text C⟩Hello
    4⟨Line D⟩⟨Line D⟩(0, 0)(10, 10)
    5⟨Rectangle B⟩⟨Rectangle B⟩1020
    6⟨Line D⟩⟨Line D⟩(0, 0)(10, 10)
  11. def draw(self) -> str:

    27def draw(self⟨Circle A⟩) -> str:28    return f"Drawing circle with radius {self.radius5}"
  12. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Circle A⟩.draw()}")
    outputRendering: Drawing circle with radius 5
  13. render_shape(circle)

    88print("--- Individual Rendering ---")89render_shape(circle⟨Circle A⟩)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text)
  14. def draw(self) -> str:

    pass 1 of 2
    38def draw(self⟨Rectangle B⟩) -> str:39    return f"Drawing rectangle {self.width10}x{self.height20}"
  15. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}")
    outputRendering: Drawing rectangle 10x20
  16. render_shape(rectangle)

    89render_shape(circle)90render_shape(rectangle⟨Rectangle B⟩)91render_shape(text⟨Text C⟩)92render_shape(line)
  17. def draw(self) -> str:

    48def draw(self⟨Text C⟩) -> str:49    return f"Drawing text: '{self.contentHello}'"
  18. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Text C⟩.draw()}")
    outputRendering: Drawing text: 'Hello'
  19. render_shape(text)

    90render_shape(rectangle)91render_shape(text⟨Text C⟩)92render_shape(line⟨Line D⟩)
  20. def draw(self) -> str:

    pass 1 of 2
    59def draw(self⟨Line D⟩) -> str:60    return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"
  21. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}")
    outputRendering: Drawing line from (0, 0) to (10, 10)
  22. 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⟩])
  23. 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 ===
  24. for shape in shapes:

    pass 1 of 2
    74print("=== Rendering All Shapes ===")75for shape⟨Rectangle B⟩ in shapes[⟨Rectangle B⟩, ⟨Line D⟩]:76    render_shape(shape⟨Rectangle B⟩)
  25. def draw(self) -> str:

    pass 2 of 2
    38def draw(self⟨Rectangle B⟩) -> str:39    return f"Drawing rectangle {self.width10}x{self.height20}"
  26. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Rectangle B⟩.draw()}")
    outputRendering: Drawing rectangle 10x20
  27. render_shape(shape)

    75for shape in shapes:76    render_shape(shape⟨Rectangle B⟩)
  28. for shape in shapes:

    pass 2 of 2
    74print("=== Rendering All Shapes ===")75for shape⟨Line D⟩ in shapes[⟨Rectangle B⟩, ⟨Line D⟩]:76    render_shape(shape⟨Line D⟩)
  29. def draw(self) -> str:

    pass 2 of 2
    59def draw(self⟨Line D⟩) -> str:60    return f"Drawing line from {self.start(0, 0)} to {self.end(10, 10)}"
  30. print(f"Rendering: {shape.draw()}")

    68"""69print(f"Rendering: {shape⟨Line D⟩.draw()}")
    outputRendering: Drawing line from (0, 0) to (10, 10)
  31. render_shape(shape)

    75for shape in shapes:76    render_shape(shape⟨Line D⟩)
  32. 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 ---
  33. self.x ← 5, self.y ← 10

    106def __init__(self⟨Point E⟩, x5, y10):107    self.x→ 5 = x5108    self.y→ 10 = y10
  34. point ← ⟨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 Interface defined by structure. No inheritance required - just have the methods.

Protocol with attributes

Protocols can require attributes too.

protocol_attributes.py
Replay: real traced execution (multi-file project)
# 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)
""")

  1. 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) #?createmotorcycle
    output=== Protocols with Attributes ===
  2. 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 #?runningstate
  3. car ← ⟨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) #?createscooter
  4. self.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 = False
  5. motorcycle ← ⟨Motorcycle B⟩

    124car = Car("Toyota", "Camry", 2022) #?createcar125motorcycle→ ⟨Motorcycle B⟩ = Motorcycle("Harley-Davidson", "Street 750", 2021) #?createmotorcycle126scooter = ElectricScooter("Xiaomi", "Mi Electric", 2023) #?createscooter
  6. self.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 #?batterylevel
  7. scooter ← ⟨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: #?loopvehicles
    output--- Test Drives ---
  8. for v in vehicles: #?loopvehicles

    pass 1 of 3
    131print("--- 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
    passvselfself._runningself.yearself.brandself.modelself._poweredself.battery_levelstatus
    1⟨Car A⟩⟨Car A⟩False2022ToyotaCamrystopped
    2⟨Motorcycle B⟩⟨Motorcycle B⟩False2021Harley-DavidsonStreet 750parked
    3⟨ElectricScooter C⟩⟨ElectricScooter C⟩2023XiaomiMi ElectricFalse100off
  9. def test_drive(vehicle: Vehicle) -> None: #?testdrive

    pass 1 of 3
    102def 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()}") #?printstart
    All 3 passes — pass 1 is the card above
    passvehicleselfself._runningself.yearself.brandself.modelself._poweredself.battery_levelstatus
    1⟨Car A⟩⟨Car A⟩False2022ToyotaCamrystopped
    2⟨Motorcycle B⟩⟨Motorcycle B⟩False2021Harley-DavidsonStreet 750parked
    3⟨ElectricScooter C⟩⟨ElectricScooter C⟩2023XiaomiMi ElectricFalse100off
  10. 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})"
  11. 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()}") #?printstop
    outputTesting: 2022 Toyota Camry (stopped)
  12. self._running ← True

    42def start(self⟨Car A⟩) -> str: #?carstart43    self._running→ True = True44    return f"{self.brandToyota} {self.modelCamry} engine started"
  13. print(f" → {vehicle.start()}") #?printstart

    104print(f"Testing: {vehicle.get_info()}") #?printinfo105print(f"  → {vehicle⟨Car A⟩.start()}") #?printstart106print(f"  → {vehicle⟨Car A⟩.stop()}") #?printstop
    output  → Toyota Camry engine started
  14. self._running ← False

    46def stop(self⟨Car A⟩) -> str: #?carstop47    self._running→ False = False48    return f"{self.brandToyota} {self.modelCamry} engine stopped"
  15. print(f" → {vehicle.stop()}") #?printstop

    105print(f"  → {vehicle.start()}") #?printstart106print(f"  → {vehicle⟨Car A⟩.stop()}") #?printstop
    output  → Toyota Camry engine stopped
  16. test_drive(v) #?calltestdrive

    132for v in vehicles: #?loopvehicles133    test_drive(v⟨Car A⟩) #?calltestdrive134    print()
  17. 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})"
  18. 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()}") #?printstop
    outputTesting: 2021 Harley-Davidson Street 750 (parked)
  19. 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!"
  20. print(f" → {vehicle.start()}") #?printstart

    104print(f"Testing: {vehicle.get_info()}") #?printinfo105print(f"  → {vehicle⟨Motorcycle B⟩.start()}") #?printstart106print(f"  → {vehicle⟨Motorcycle B⟩.stop()}") #?printstop
    output  → Harley-Davidson Street 750 roars to life!
  21. self._running ← False

    68def stop(self⟨Motorcycle B⟩) -> str: #?motorcyclestop69    self._running→ False = False70    return f"{self.brandHarley-Davidson} {self.modelStreet 750} goes silent"
  22. print(f" → {vehicle.stop()}") #?printstop

    105print(f"  → {vehicle.start()}") #?printstart106print(f"  → {vehicle⟨Motorcycle B⟩.stop()}") #?printstop
    output  → Harley-Davidson Street 750 goes silent
  23. test_drive(v) #?calltestdrive

    132for v in vehicles: #?loopvehicles133    test_drive(v⟨Motorcycle B⟩) #?calltestdrive134    print()
  24. 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)"
  25. 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()}") #?printstop
    outputTesting: 2023 Xiaomi Mi Electric (off, 100% battery)
  26. self._powered ← True

    87def start(self⟨ElectricScooter C⟩) -> str: #?scooterstart88    self._powered→ True = True89    return f"{self.brandXiaomi} {self.modelMi Electric} silently powers on"
  27. print(f" → {vehicle.start()}") #?printstart

    104print(f"Testing: {vehicle.get_info()}") #?printinfo105print(f"  → {vehicle⟨ElectricScooter C⟩.start()}") #?printstart106print(f"  → {vehicle⟨ElectricScooter C⟩.stop()}") #?printstop
    output  → Xiaomi Mi Electric silently powers on
  28. self._powered ← False

    91def stop(self⟨ElectricScooter C⟩) -> str: #?scooterstop92    self._powered→ False = False93    return f"{self.brandXiaomi} {self.modelMi Electric} powers off"
  29. print(f" → {vehicle.stop()}") #?printstop

    105print(f"  → {vehicle.start()}") #?printstart106print(f"  → {vehicle⟨ElectricScooter C⟩.stop()}") #?printstop
    output  → Xiaomi Mi Electric powers off
  30. test_drive(v) #?calltestdrive

    132for v in vehicles: #?loopvehicles133    test_drive(v⟨ElectricScooter C⟩) #?calltestdrive134    print()
  31. vehicle_summary(vehicles) #?callsummary

    136# Summary - accessing protocol attributes #?summaryDemo137vehicle_summary(vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]) #?callsummary
  32. def 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: #?iteratevehicles
    output
    === Vehicle Fleet ===
  33. for v in vehicles: #?iteratevehicles

    pass 1 of 3
    111print("\n=== Vehicle Fleet ===")112for v⟨Car A⟩ in vehicles[⟨Car A⟩, ⟨Motorcycle B⟩, ⟨ElectricScooter C⟩]: #?iteratevehicles113    print(f"  {v.year2022} {v.brandToyota} {v.modelCamry}") #?accessattributes
    output  2022 Toyota Camry
    All 3 passes — pass 1 is the card above
    passvv.yearv.brandv.model
    1⟨Car A⟩2022ToyotaCamry
    2⟨Motorcycle B⟩2021Harley-DavidsonStreet 750
    3⟨ElectricScooter C⟩2023XiaomiMi Electric
  34. 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 ---
  35. 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()] #?filterbybrand
  36. toyotas ← [⟨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 ---
  37. self.brand ← Trek

    150def __init__(self⟨Bicycle D⟩, brandTrek): #?bicycleinit151    self.brand→ Trek = brandTrek152    # Missing: model, year attributes #?missingattrs
  38. bicycle ← ⟨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.py
Replay: real traced execution (multi-file project)
# 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
""")

  1. 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: #?iterateobjects
    output=== Runtime Checkable Protocols ===
    --- isinstance() with @runtime_checkable ---
    
    Walkable check (has walk() method?):
  2. name ← Dog, is_walkable ← True

    pass 1 of 4
    83print("\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) = True
    All 4 passes — pass 1 is the card above
    passobjnameis_walkable
    1⟨Dog A⟩DogTrue
    2⟨Fish B⟩FishFalse
    3⟨Duck C⟩DuckTrue
    4⟨Robot D⟩RobotTrue
  3. print(" Swimmable check (has swim() method?):")

    89print("\nSwimmable check (has swim() method?):")90for obj in objects:
    output
    Swimmable check (has swim() method?):
  4. name ← Dog, is_swimmable ← False

    pass 1 of 4
    89print("\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) = False
    All 4 passes — pass 1 is the card above
    passobjnameis_swimmable
    1⟨Dog A⟩DogFalse
    2⟨Fish B⟩FishTrue
    3⟨Duck C⟩DuckTrue
    4⟨Robot D⟩RobotFalse
  5. print(" --- isinstance() without @runtime_checkable ---")

    95# Test with non-runtime-checkable protocol #?testregular96print("\n--- isinstance() without @runtime_checkable ---")97try:
    output
    --- isinstance() without @runtime_checkable ---
  6. 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}")
  7. 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!
  8. 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 ---
  9. 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'>)] #?filterwalkers
  10. walkers ← [⟨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']
  11. 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'>)] #?filterswimmers
  12. swimmers ← [⟨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: #?loopobjects
    outputSwimmers: ['Fish', 'Duck']
    
    --- Process Only Walkers ---
  13. for obj in objects: #?loopobjects

    pass 1 of 4
    122print("\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()}") #?callwalk
    All 4 passes — pass 1 is the card above
    passobjself
    1⟨Dog A⟩⟨Dog A⟩
    2⟨Fish B⟩
    3⟨Duck C⟩⟨Duck C⟩
    4⟨Robot D⟩⟨Robot D⟩
  14. if isinstance(obj, Walkable): #?checkbeforeuse

    pass 1 of 3
    123for obj in objects: #?loopobjects124    if isinstance(obj⟨Dog A⟩, Walkable<class '__main__.Walkable'>): #?checkbeforeuse125        print(f"  {type(obj⟨Dog A⟩).__name__}: {obj.walk()}") #?callwalk
    All 3 passes — pass 1 is the card above
    passobjself
    1⟨Dog A⟩⟨Dog A⟩
    2⟨Duck C⟩⟨Duck C⟩
    3⟨Robot D⟩⟨Robot D⟩
  15. def walk(self) -> str: #?dogwalk

    37def walk(self⟨Dog A⟩) -> str: #?dogwalk38    return "Dog walks on four legs"
  16. print(f" {type(obj).__name__}: {obj.walk()}") #?callwalk

    124if isinstance(obj, Walkable): #?checkbeforeuse125    print(f"  {type(obj⟨Dog A⟩).__name__}: {obj.walk()}") #?callwalk
    output  Dog: Dog walks on four legs
  17. def walk(self) -> str: #?duckwalk

    pass 1 of 2
    54def walk(self⟨Duck C⟩) -> str: #?duckwalk55    return "Duck waddles"
  18. print(f" {type(obj).__name__}: {obj.walk()}") #?callwalk

    124if isinstance(obj, Walkable): #?checkbeforeuse125    print(f"  {type(obj⟨Duck C⟩).__name__}: {obj.walk()}") #?callwalk
    output  Duck: Duck waddles
  19. def walk(self) -> str: #?robotwalk

    67def walk(self⟨Robot D⟩) -> str: #?robotwalk68    return "Robot walks mechanically"
  20. print(f" {type(obj).__name__}: {obj.walk()}") #?callwalk

    124if isinstance(obj, Walkable): #?checkbeforeuse125    print(f"  {type(obj⟨Robot D⟩).__name__}: {obj.walk()}") #?callwalk
    output  Robot: Robot walks mechanically
  21. print(" --- Process Only Swimmers ---")

    127print("\n--- Process Only Swimmers ---")128for obj in objects:
    output
    --- Process Only Swimmers ---
  22. for obj in objects:

    pass 1 of 4
    127print("\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()}") #?callswim
    All 4 passes — pass 1 is the card above
    passobjSwimmableself
    1⟨Dog A⟩
    2⟨Fish B⟩<class '__main__.Swimmable'>⟨Fish B⟩
    3⟨Duck C⟩<class '__main__.Swimmable'>⟨Duck C⟩
    4⟨Robot D⟩
  23. if isinstance(obj, Swimmable):

    pass 1 of 2
    128for obj in objects:129    if isinstance(obj⟨Fish B⟩, Swimmable<class '__main__.Swimmable'>):130        print(f"  {type(obj⟨Fish B⟩).__name__}: {obj.swim()}") #?callswim
  24. def swim(self) -> str: #?fishswim

    47def swim(self⟨Fish B⟩) -> str: #?fishswim48    return "Fish swims with fins"
  25. print(f" {type(obj).__name__}: {obj.swim()}") #?callswim

    129if isinstance(obj, Swimmable):130    print(f"  {type(obj⟨Fish B⟩).__name__}: {obj.swim()}") #?callswim
    output  Fish: Fish swims with fins
  26. if isinstance(obj, Swimmable):

    pass 2 of 2
    128for obj in objects:129    if isinstance(obj⟨Duck C⟩, Swimmable<class '__main__.Swimmable'>):130        print(f"  {type(obj⟨Duck C⟩).__name__}: {obj.swim()}") #?callswim
  27. def swim(self) -> str: #?duckswim

    pass 1 of 2
    57def swim(self⟨Duck C⟩) -> str: #?duckswim58    return "Duck paddles on water"
  28. print(f" {type(obj).__name__}: {obj.swim()}") #?callswim

    129if isinstance(obj, Swimmable):130    print(f"  {type(obj⟨Duck C⟩).__name__}: {obj.swim()}") #?callswim
    output  Duck: Duck paddles on water
  29. print(" --- 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) ---
  30. for obj in objects:

    pass 1 of 4
    133print("\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
    passobjWalkableSwimmableselfname
    1⟨Dog A⟩
    2⟨Fish B⟩
    3⟨Duck C⟩<class '__main__.Walkable'><class '__main__.Swimmable'>⟨Duck C⟩Duck
    4⟨Robot D⟩
  31. 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:
  32. def walk(self) -> str: #?duckwalk

    pass 2 of 2
    54def walk(self⟨Duck C⟩) -> str: #?duckwalk55    return "Duck waddles"
  33. print(f" - {obj.walk()}")

    137print(f"  {name} can:")138print(f"    - {obj⟨Duck C⟩.walk()}")139print(f"    - {obj⟨Duck C⟩.swim()}")
    output    - Duck waddles
  34. def swim(self) -> str: #?duckswim

    pass 2 of 2
    57def swim(self⟨Duck C⟩) -> str: #?duckswim58    return "Duck paddles on water"
  35. print(f" - {obj.swim()}")

    138print(f"    - {obj.walk()}")139print(f"    - {obj⟨Duck C⟩.swim()}")
    output    - Duck paddles on water
  36. print(" === @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.

runtime_checkable Allows isinstance() with Protocol. Checks method presence at runtime.

Protocol vs ABC

When to use each approach.

protocol_vs_abc.py
Replay: real traced execution (multi-file project)
# 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)
""")

  1. 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) #?createrectabc
    output=== Protocol vs ABC ===
    --- ABC (Abstract Base Class) ---
    --- Protocol ---
    --- Comparison ---
  2. self.radius ← 5

    33def __init__(self⟨CircleABC A⟩, radius5): #?abccircleinit34    self.radius→ 5 = radius5
  3. circle_abc ← ⟨CircleABC A⟩

    121# Create instances #?createinstances122circle_abc→ ⟨CircleABC A⟩ = CircleABC(5) #?createcircleabc123rect_abc = RectangleABC(4, 6) #?createrectabc
  4. self.width ← 4, self.height ← 6

    46def __init__(self⟨RectangleABC B⟩, width4, height6): #?abcrectangleinit47    self.width→ 4 = width448    self.height→ 6 = height6
  5. rect_abc ← ⟨RectangleABC B⟩

    122circle_abc = CircleABC(5) #?createcircleabc123rect_abc→ ⟨RectangleABC B⟩ = RectangleABC(4, 6) #?createrectabc124125circle_prot = CircleProtocol(5) #?createcircleprot126rect_prot = RectangleProtocol(4, 6) #?createrectprot
  6. self.radius ← 5

    80def __init__(self⟨CircleProtocol C⟩, radius5): #?protocolcircleinit81    self.radius→ 5 = radius5
  7. circle_prot ← ⟨CircleProtocol C⟩

    125circle_prot→ ⟨CircleProtocol C⟩ = CircleProtocol(5) #?createcircleprot126rect_prot = RectangleProtocol(4, 6) #?createrectprot
  8. self.width ← 4, self.height ← 6

    93def __init__(self⟨RectangleProtocol D⟩, width4, height6): #?protocolrectangleinit94    self.width→ 4 = width495    self.height→ 6 = height6
  9. rect_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:
  10. def calculate_abc(shape: ShapeABC) -> None: #?calculateabc

    pass 1 of 2
    107# 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}")
  11. def area(self) -> float: #?abccirclearea

    pass 1 of 2
    36def area(self⟨CircleABC A⟩) -> float: #?abccirclearea37    return CircleABC.PI3.14159 * self.radius5 ** 2
  12. print(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.54
  13. def perimeter(self) -> float: #?abccircleperimeter

    pass 1 of 2
    39def perimeter(self⟨CircleABC A⟩) -> float: #?abccircleperimeter40    return 2 * CircleABC.PI3.14159 * self.radius5
  14. print(f" Perimeter: {shape.perimeter():.2f}")

    110print(f"  Area: {shape.area():.2f}")111print(f"  Perimeter: {shape⟨CircleABC A⟩.perimeter():.2f}")
    output  Perimeter: 31.42
  15. calculate_abc(circle_abc) #?calccircleabc

    130print("Circle:")131calculate_abc(circle_abc⟨CircleABC A⟩) #?calccircleabc132print("Rectangle:")133calculate_abc(rect_abc⟨RectangleABC B⟩) #?calcrectabc
    outputRectangle:
  16. def calculate_abc(shape: ShapeABC) -> None: #?calculateabc

    pass 2 of 2
    107# 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}")
  17. def area(self) -> float: #?abcrectanglearea

    50def area(self⟨RectangleABC B⟩) -> float: #?abcrectanglearea51    return self.width4 * self.height6
  18. print(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.00
  19. def perimeter(self) -> float: #?abcrectangleperimeter

    53def perimeter(self⟨RectangleABC B⟩) -> float: #?abcrectangleperimeter54    return 2 * (self.width4 + self.height6)
  20. print(f" Perimeter: {shape.perimeter():.2f}")

    110print(f"  Area: {shape.area():.2f}")111print(f"  Perimeter: {shape⟨RectangleABC B⟩.perimeter():.2f}")
    output  Perimeter: 20.00
  21. calculate_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:
  22. def calculate_protocol(shape: ShapeProtocol) -> None: #?calculateproto…

    pass 1 of 3
    114# 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
    passshapeselfCircleProtocol.PIself.radiusself.widthself.heightCircleABC.PI
    1⟨CircleProtocol C⟩⟨CircleProtocol C⟩3.141595
    2⟨RectangleProtocol D⟩⟨RectangleProtocol D⟩46
    3⟨CircleABC A⟩⟨CircleABC A⟩53.14159
  23. def area(self) -> float: #?protocolcirclearea

    83def area(self⟨CircleProtocol C⟩) -> float: #?protocolcirclearea84    return CircleProtocol.PI3.14159 * self.radius5 ** 2
  24. print(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.54
  25. def perimeter(self) -> float: #?protocolcircleperimeter

    86def perimeter(self⟨CircleProtocol C⟩) -> float: #?protocolcircleperimeter87    return 2 * CircleProtocol.PI3.14159 * self.radius5
  26. print(f" Perimeter: {shape.perimeter():.2f}")

    117print(f"  Area: {shape.area():.2f}")118print(f"  Perimeter: {shape⟨CircleProtocol C⟩.perimeter():.2f}")
    output  Perimeter: 31.42
  27. calculate_protocol(circle_prot) #?calccircleprot

    139print("Circle:")140calculate_protocol(circle_prot⟨CircleProtocol C⟩) #?calccircleprot141print("Rectangle:")142calculate_protocol(rect_prot⟨RectangleProtocol D⟩) #?calcrectprot
    outputRectangle:
  28. def area(self) -> float: #?protocolrectanglearea

    97def area(self⟨RectangleProtocol D⟩) -> float: #?protocolrectanglearea98    return self.width4 * self.height6
  29. print(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.00
  30. def perimeter(self) -> float: #?protocolrectangleperimeter

    100def perimeter(self⟨RectangleProtocol D⟩) -> float: #?protocolrectangleperimeter101    return 2 * (self.width4 + self.height6)
  31. print(f" Perimeter: {shape.perimeter():.2f}")

    117print(f"  Area: {shape.area():.2f}")118print(f"  Perimeter: {shape⟨RectangleProtocol D⟩.perimeter():.2f}")
    output  Perimeter: 20.00
  32. calculate_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 ---
  33. 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'
  34. 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:
  35. def area(self) -> float: #?abccirclearea

    pass 2 of 2
    36def area(self⟨CircleABC A⟩) -> float: #?abccirclearea37    return CircleABC.PI3.14159 * self.radius5 ** 2
  36. print(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.54
  37. def perimeter(self) -> float: #?abccircleperimeter

    pass 2 of 2
    39def perimeter(self⟨CircleABC A⟩) -> float: #?abccircleperimeter40    return 2 * CircleABC.PI3.14159 * self.radius5
  38. print(f" Perimeter: {shape.perimeter():.2f}")

    117print(f"  Area: {shape.area():.2f}")118print(f"  Perimeter: {shape⟨CircleABC A⟩.perimeter():.2f}")
    output  Perimeter: 31.42
  39. calculate_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.

structural typing Type compatibility based on structure (methods/attributes), not inheritance.

Built-in protocols

Standard library protocols you use every day.

common_protocols.py
Replay: real traced execution (multi-file project)
# 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]
""")

  1. 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)}") #?printnumbers
    output=== Common Protocol Patterns ===
    --- Iterable Pattern ---
  2. self.start ← 1, self.end ← 5

    21def __init__(self⟨NumberRange A⟩, start1, end5): #?rangeinit22    self.start→ 1 = start123    self.end→ 5 = end5
  3. numbers ← ⟨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)}") #?checkiterable
  4. current ← 1

    25def __iter__(self⟨NumberRange A⟩): #?rangeiter26    current→ 1 = self.start127    while current <= self.end: #?rangeloop
  5. current ← 2

    pass 1 of 5
    26current = self.start27while current1 <= self.end5: #?rangeloop28    yield current1 #?yieldcurrent29    current→ 2 += 1
    All 5 passes — pass 1 is the card above
    passcurrent
    11 2
    22 3
    33 4
    44 5
    55 6
  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) #?createtriple
    outputNumberRange(1, 5): [1, 2, 3, 4, 5]
    Is Iterable: True
    --- Callable Pattern ---
  7. self.factor ← 2

    pass 1 of 2
    53def __init__(self⟨Multiplier B⟩, factor2): #?multiplierinit54    self.factor→ 2 = factor2
  8. double ← ⟨Multiplier B⟩

    60double→ ⟨Multiplier B⟩ = Multiplier(2) #?createdouble61triple = Multiplier(3) #?createtriple
  9. self.factor ← 3

    pass 2 of 2
    53def __init__(self⟨Multiplier C⟩, factor3): #?multiplierinit54    self.factor→ 3 = factor3
  10. triple ← ⟨Multiplier C⟩

    60double = Multiplier(2) #?createdouble61triple→ ⟨Multiplier C⟩ = Multiplier(3) #?createtriple6263print(f"double(5) = {double(5)}") #?calldouble64print(f"triple(5) = {triple(5)}") #?calltriple
  11. def __call__(self, value): #?multipliercall

    pass 1 of 2
    56def __call__(self⟨Multiplier B⟩, value5): #?multipliercall57    return value5 * self.factor2
  12. print(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)}") #?checkcallable
    outputdouble(5) = 10
  13. def __call__(self, value): #?multipliercall

    pass 2 of 2
    56def __call__(self⟨Multiplier C⟩, value5): #?multipliercall57    return value5 * self.factor3
  14. print(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") #?addsong1
    outputtriple(5) = 15
    Is Callable: True
    square is Callable: True
    --- Sized Pattern ---
  15. self.name ← My Mix, self.songs ← []

    89def __init__(self⟨Playlist E⟩, nameMy Mix): #?playlistinit90    self.name→ My Mix = nameMy Mix91    self.songs→ [] = [] #?playlistsongs
  16. playlist ← ⟨Playlist E⟩

    100playlist→ ⟨Playlist E⟩ = Playlist("My Mix") #?createplaylist101playlist⟨Playlist E⟩.add("Song A") #?addsong1102playlist.add("Song B") #?addsong2
  17. self.songs ← ['Song A']

    pass 1 of 3
    93def add(self⟨Playlist E⟩, songSong A): #?playlistadd94    self.songs→ ['Song A'].append(songSong A)
    All 3 passes — pass 1 is the card above
    passsongself.songs
    1Song A[] ['Song A']
    2Song B['Song A'] ['Song A', 'Song B']
    3Song C['Song A', 'Song B'] ['Song A', 'Song B', 'Song C']
  18. 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") #?addsong3
  19. playlist.add("Song B") #?addsong2

    101playlist.add("Song A") #?addsong1102playlist⟨Playlist E⟩.add("Song B") #?addsong2103playlist⟨Playlist E⟩.add("Song C") #?addsong3
  20. playlist.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)}") #?checksized
  21. def __len__(self) -> int: #?playlistlen

    96def __len__(self⟨Playlist E⟩) -> int: #?playlistlen97    return len(self.songs['Song A', 'Song B', 'Song C'])
  22. 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") #?addapple
    outputPlaylist 'My Mix' has 3 songs
    Is Sized: True
    --- Container Pattern (supports 'in') ---
  23. self.items ← []

    124def __init__(self⟨ShoppingCart F⟩): #?cartinit125    self.items→ [] = [] #?cartitems
  24. cart ← ⟨ShoppingCart F⟩

    134cart→ ⟨ShoppingCart F⟩ = ShoppingCart() #?createcart135cart⟨ShoppingCart F⟩.add("Apple") #?addapple136cart.add("Banana") #?addbanana
  25. self.items ← ['Apple']

    pass 1 of 2
    127def add(self⟨ShoppingCart F⟩, itemApple): #?cartadd128    self.items→ ['Apple'].append(itemApple)
  26. cart.add("Apple") #?addapple

    134cart = ShoppingCart() #?createcart135cart⟨ShoppingCart F⟩.add("Apple") #?addapple136cart⟨ShoppingCart F⟩.add("Banana") #?addbanana
  27. self.items ← ['Apple', 'Banana']

    pass 2 of 2
    127def add(self⟨ShoppingCart F⟩, itemBanana): #?cartadd128    self.items→ ['Apple', 'Banana'].append(itemBanana)
  28. 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}") #?incheckorange
  29. def __contains__(self, item) -> bool: #?cartcontains

    pass 1 of 2
    130def __contains__(self⟨ShoppingCart F⟩, itemApple) -> bool: #?cartcontains131    return itemApple in self.items['Apple', 'Banana']
  30. 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)}") #?checkcontainer
    output'Apple' in cart: True
  31. def __contains__(self, item) -> bool: #?cartcontains

    pass 2 of 2
    130def __contains__(self⟨ShoppingCart F⟩, itemOrange) -> bool: #?cartcontains131    return itemOrange in self.items['Apple', 'Banana']
  32. 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'>)}") #?checkcontext
    output'Orange' in cart: False
    Is Container: True
    --- Context Manager Pattern ---
  33. self.name ← test, self.start ← 0

    pass 1 of 2
    161def __init__(self⟨Timer G⟩, nametest): #?timerinit162    self.name→ test = nametest163    self.start→ 0 = 0 #?timerstart
  34. print(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerP…

    176print(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerProtocol<class '__main__.ContextManagerProtocol'>)}") #?checkcontext
    outputIs Context Manager: True
  35. self.name ← Processing, self.start ← 0

    pass 2 of 2
    161def __init__(self⟨Timer G⟩, nameProcessing): #?timerinit162    self.name→ Processing = nameProcessing163    self.start→ 0 = 0 #?timerstart
  36. self.start ← 1000.0

    165def __enter__(self⟨Timer G⟩): #?timerenter166    self.start→ 1000.0 = 1000.0167    print(f"[{self.nameProcessing}] Starting...") #?printstart168    return self #?returnself
    output[Processing] Starting...
  37. 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: 49995000
  38. elapsed ← 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 #?returnfalse
    output[Processing] Completed in 0.0010 seconds
  39. print()

    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") #?storeaddA
    output--- Combining Multiple Protocols ---
  40. self.data ← []

    191def __init__(self⟨DataStore H⟩): #?datastoreinit192    self.data→ [] = []
  41. store ← ⟨DataStore H⟩

    210store→ ⟨DataStore H⟩ = DataStore() #?createstore211store⟨DataStore H⟩.add("A") #?storeaddA212store.add("B") #?storeaddB
  42. self.data ← ['A']

    pass 1 of 3
    194def add(self⟨DataStore H⟩, itemA): #?datastoreadd195    self.data→ ['A'].append(itemA)
    All 3 passes — pass 1 is the card above
    passitemself.data
    1A[] ['A']
    2B['A'] ['A', 'B']
    3C['A', 'B'] ['A', 'B', 'C']
  43. store.add("A") #?storeaddA

    210store = DataStore() #?createstore211store⟨DataStore H⟩.add("A") #?storeaddA212store⟨DataStore H⟩.add("B") #?storeaddB213store.add("C") #?storeaddC
  44. store.add("B") #?storeaddB

    211store.add("A") #?storeaddA212store⟨DataStore H⟩.add("B") #?storeaddB213store⟨DataStore H⟩.add("C") #?storeaddC
  45. store.add("C") #?storeaddC

    212store.add("B") #?storeaddB213store⟨DataStore H⟩.add("C") #?storeaddC214215print(f"DataStore: {list(store⟨DataStore H⟩)}") #?liststore216print(f"Length: {len(store)}") #?lenstore
  46. def __iter__(self): #?datastoreiter

    197# Iterable #?iterableimpl198def __iter__(self⟨DataStore H⟩): #?datastoreiter199    return iter(self.data['A', 'B', 'C'])
  47. def __len__(self) -> int: #?datastorelen

    pass 1 of 2
    201# Sized #?sizedimpl202def __len__(self⟨DataStore H⟩) -> int: #?datastorelen203    return len(self.data['A', 'B', 'C'])
  48. 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}") #?instore
    outputDataStore: ['A', 'B', 'C']
  49. def __len__(self) -> int: #?datastorelen

    pass 2 of 2
    201# Sized #?sizedimpl202def __len__(self⟨DataStore H⟩) -> int: #?datastorelen203    return len(self.data['A', 'B', 'C'])
  50. 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⟩}") #?instore
    outputLength: 3
  51. def __contains__(self, item) -> bool: #?datastorecontains

    205# Container #?containerimpl206def __contains__(self⟨DataStore H⟩, itemB) -> bool: #?datastorecontains207    return itemB in self.data['A', 'B', 'C']
  52. 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