You want a function that accepts "anything with a read() method". With ABC, the object must inherit from your base class. Protocols check structure instead - if it has read(), it's compatible. Static type checking meets duck typing.

Basic protocol

Define an interface by structure.

basic_protocol.py
# Basic Protocols

from typing import Protocol, runtime_checkable

# Define a protocol
@runtime_checkable
class Drawable(Protocol):
    """
    Protocol for drawable objects.
    Any class with draw() method is compatible.
    """
    
    def draw(self) -> str:
        """Draw the object and return description."""
        ...


# Classes that satisfy the protocol
# Note: NO inheritance from Drawable!

class Circle:
    """Circle class - has draw() method."""
    
    def __init__(self, radius):
        self.radius = radius
    
    def draw(self) -> str:
        return f"Drawing circle with radius {self.radius}"


class Rectangle:
    """Rectangle class - has draw() method."""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def draw(self) -> str:
        return f"Drawing rectangle {self.width}x{self.height}"


class Text:
    """Text class - has draw() method."""
    
    def __init__(self, content):
        self.content = content
    
    def draw(self) -> str:
        return f"Drawing text: '{self.content}'"


class Line:
    """Line class - has draw() method."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def draw(self) -> str:
        return f"Drawing line from {self.start} to {self.end}"


# Function using the protocol as type hint
def render_shape(shape: Drawable) -> None:
    """
    Render any Drawable object.
    Works with Circle, Rectangle, Text, Line - anything with draw().
    """
    print(f"Rendering: {shape.draw()}")


def render_all(shapes: list[Drawable]) -> None:
    """Render multiple drawable objects."""
    print("=== Rendering All Shapes ===")
    for shape in shapes:
        render_shape(shape)


print("=== Basic Protocols ===\n")

# Create objects (none inherit from Drawable!)
circle = Circle(5)
rectangle = Rectangle(10, 20)
text = Text("Hello")
line = Line((0, 0), (10, 10))

# All work with render_shape()
print("--- Individual Rendering ---")
render_shape(circle)
render_shape(rectangle)
render_shape(text)
render_shape(line)

print()

# Render all together
shapes = 
render_all(shapes)

# Object without draw() method
print("\n--- Non-Drawable Object ---")

class Point:
    """Point has no draw() method."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(5, 10)

# This would cause a type error (if using type checker)
# but still runs because Python is dynamically typed
print("Point object created (no draw method)")
print("Type checkers would flag: render_shape(point)")

# Demonstrate that it's structural, not nominal
print("\n--- Structural Typing Proof ---")
print(f"Circle inherits from Drawable: {issubclass(Circle, Drawable) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")

print("\n=== Protocol Key Points ===")
print("""
1. Define interface with Protocol:
   class MyProtocol(Protocol):
       def method(self) -> Type: ...

2. No inheritance required:
   - Classes just need matching methods
   - "Structural subtyping"

3. Works with type checkers:
   - mypy, pyright, etc.
   - Catches mismatches at development time

4. ... (ellipsis) vs pass:
   - Both work in protocols
   - ... is convention for "to be implemented"

5. Duck typing formalized:
   - "If it walks like a duck..."
   - Now with type hints!
""")

# Basic Protocols

from typing import Protocol, runtime_checkable

# Define a protocol
@runtime_checkable
class Drawable(Protocol):
    """
    Protocol for drawable objects.
    Any class with draw() method is compatible.
    """
    
    def draw(self) -> str:
        """Draw the object and return description."""
        ...


# Classes that satisfy the protocol
# Note: NO inheritance from Drawable!

class Circle:
    """Circle class - has draw() method."""
    
    def __init__(self, radius):
        self.radius = radius
    
    def draw(self) -> str:
        return f"Drawing circle with radius {self.radius}"


class Rectangle:
    """Rectangle class - has draw() method."""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def draw(self) -> str:
        return f"Drawing rectangle {self.width}x{self.height}"


class Text:
    """Text class - has draw() method."""
    
    def __init__(self, content):
        self.content = content
    
    def draw(self) -> str:
        return f"Drawing text: '{self.content}'"


class Line:
    """Line class - has draw() method."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def draw(self) -> str:
        return f"Drawing line from {self.start} to {self.end}"


# Function using the protocol as type hint
def render_shape(shape: Drawable) -> None:
    """
    Render any Drawable object.
    Works with Circle, Rectangle, Text, Line - anything with draw().
    """
    print(f"Rendering: {shape.draw()}")


def render_all(shapes: list[Drawable]) -> None:
    """Render multiple drawable objects."""
    print("=== Rendering All Shapes ===")
    for shape in shapes:
        render_shape(shape)


print("=== Basic Protocols ===\n")

# Create objects (none inherit from Drawable!)
circle = Circle(5)
rectangle = Rectangle(10, 20)
text = Text("Hello")
line = Line((0, 0), (10, 10))

# All work with render_shape()
print("--- Individual Rendering ---")
render_shape(circle)
render_shape(rectangle)
render_shape(text)
render_shape(line)

print()

# Render all together
shapes = 
render_all(shapes)

# Object without draw() method
print("\n--- Non-Drawable Object ---")

class Point:
    """Point has no draw() method."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(5, 10)

# This would cause a type error (if using type checker)
# but still runs because Python is dynamically typed
print("Point object created (no draw method)")
print("Type checkers would flag: render_shape(point)")

# Demonstrate that it's structural, not nominal
print("\n--- Structural Typing Proof ---")
print(f"Circle inherits from Drawable: {issubclass(Circle, Drawable) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")

print("\n=== Protocol Key Points ===")
print("""
1. Define interface with Protocol:
   class MyProtocol(Protocol):
       def method(self) -> Type: ...

2. No inheritance required:
   - Classes just need matching methods
   - "Structural subtyping"

3. Works with type checkers:
   - mypy, pyright, etc.
   - Catches mismatches at development time

4. ... (ellipsis) vs pass:
   - Both work in protocols
   - ... is convention for "to be implemented"

5. Duck typing formalized:
   - "If it walks like a duck..."
   - Now with type hints!
""")

# Basic Protocols

from typing import Protocol, runtime_checkable

# Define a protocol
@runtime_checkable
class Drawable(Protocol):
    """
    Protocol for drawable objects.
    Any class with draw() method is compatible.
    """
    
    def draw(self) -> str:
        """Draw the object and return description."""
        ...


# Classes that satisfy the protocol
# Note: NO inheritance from Drawable!

class Circle:
    """Circle class - has draw() method."""
    
    def __init__(self, radius):
        self.radius = radius
    
    def draw(self) -> str:
        return f"Drawing circle with radius {self.radius}"


class Rectangle:
    """Rectangle class - has draw() method."""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def draw(self) -> str:
        return f"Drawing rectangle {self.width}x{self.height}"


class Text:
    """Text class - has draw() method."""
    
    def __init__(self, content):
        self.content = content
    
    def draw(self) -> str:
        return f"Drawing text: '{self.content}'"


class Line:
    """Line class - has draw() method."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def draw(self) -> str:
        return f"Drawing line from {self.start} to {self.end}"


# Function using the protocol as type hint
def render_shape(shape: Drawable) -> None:
    """
    Render any Drawable object.
    Works with Circle, Rectangle, Text, Line - anything with draw().
    """
    print(f"Rendering: {shape.draw()}")


def render_all(shapes: list[Drawable]) -> None:
    """Render multiple drawable objects."""
    print("=== Rendering All Shapes ===")
    for shape in shapes:
        render_shape(shape)


print("=== Basic Protocols ===\n")

# Create objects (none inherit from Drawable!)
circle = Circle(5)
rectangle = Rectangle(10, 20)
text = Text("Hello")
line = Line((0, 0), (10, 10))

# All work with render_shape()
print("--- Individual Rendering ---")
render_shape(circle)
render_shape(rectangle)
render_shape(text)
render_shape(line)

print()

# Render all together
shapes = 
render_all(shapes)

# Object without draw() method
print("\n--- Non-Drawable Object ---")

class Point:
    """Point has no draw() method."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(5, 10)

# This would cause a type error (if using type checker)
# but still runs because Python is dynamically typed
print("Point object created (no draw method)")
print("Type checkers would flag: render_shape(point)")

# Demonstrate that it's structural, not nominal
print("\n--- Structural Typing Proof ---")
print(f"Circle inherits from Drawable: {issubclass(Circle, Drawable) if hasattr(Drawable, '__subclasshook__') else 'No (not runtime checkable)'}")

print("\n=== Protocol Key Points ===")
print("""
1. Define interface with Protocol:
   class MyProtocol(Protocol):
       def method(self) -> Type: ...

2. No inheritance required:
   - Classes just need matching methods
   - "Structural subtyping"

3. Works with type checkers:
   - mypy, pyright, etc.
   - Catches mismatches at development time

4. ... (ellipsis) vs pass:
   - Both work in protocols
   - ... is convention for "to be implemented"

5. Duck typing formalized:
   - "If it walks like a duck..."
   - Now with type hints!
""")

from typing import Protocol. Define methods the type must have.

Protocol Interface defined by structure. No inheritance required - just have the methods.

Protocol with attributes

Protocols can require attributes too.

protocol_attributes.py
# Protocols with Attributes and Methods

from typing import Protocol

# Protocol with both methods and attributes
class Vehicle(Protocol):
    """
    Protocol for vehicles.
    Requires specific attributes AND methods.
    """
    
    # Attribute declarations
    brand: str
    model: str
    year: int
    
    # Method declarations
    def start(self) -> str:
        """Start the vehicle."""
        ...
    
    def stop(self) -> str:
        """Stop the vehicle."""
        ...
    
    def get_info(self) -> str:
        """Get vehicle info."""
        ...


# Classes implementing the protocol

class Car:
    """Car satisfies Vehicle protocol."""
    
    def __init__(self, brand: str, model: str, year: int):
        self.brand = brand  # Required attribute
        self.model = model  # Required attribute
        self.year = year    # Required attribute
        self._running = False
    
    def start(self) -> str:
        self._running = True
        return f"{self.brand} {self.model} engine started"
    
    def stop(self) -> str:
        self._running = False
        return f"{self.brand} {self.model} engine stopped"
    
    def get_info(self) -> str:
        status = "running" if self._running else "stopped"
        return f"{self.year} {self.brand} {self.model} ({status})"


class Motorcycle:
    """Motorcycle satisfies Vehicle protocol."""
    
    def __init__(self, brand: str, model: str, year: int):
        self.brand = brand
        self.model = model
        self.year = year
        self._running = False
    
    def start(self) -> str:
        self._running = True
        return f"{self.brand} {self.model} roars to life!"
    
    def stop(self) -> str:
        self._running = False
        return f"{self.brand} {self.model} goes silent"
    
    def get_info(self) -> str:
        status = "running" if self._running else "parked"
        return f"{self.year} {self.brand} {self.model} ({status})"


class ElectricScooter:
    """Electric scooter - also satisfies Vehicle protocol."""
    
    def __init__(self, brand: str, model: str, year: int):
        self.brand = brand
        self.model = model
        self.year = year
        self._powered = False
        self.battery_level = 100
    
    def start(self) -> str:
        self._powered = True
        return f"{self.brand} {self.model} silently powers on"
    
    def stop(self) -> str:
        self._powered = False
        return f"{self.brand} {self.model} powers off"
    
    def get_info(self) -> str:
        status = "on" if self._powered else "off"
        return f"{self.year} {self.brand} {self.model} ({status}, {self.battery_level}% battery)"


# Functions using Vehicle protocol

def test_drive(vehicle: Vehicle) -> None:
    """Test drive any vehicle."""
    print(f"Testing: {vehicle.get_info()}")
    print(f"  → {vehicle.start()}")
    print(f"  → {vehicle.stop()}")


def vehicle_summary(vehicles: list[Vehicle]) -> None:
    """Print summary of all vehicles."""
    print("\n=== Vehicle Fleet ===")
    for v in vehicles:
        print(f"  {v.year} {v.brand} {v.model}")


def find_by_brand(vehicles: list[Vehicle], brand: str) -> list[Vehicle]:
    """Find vehicles by brand."""
    return [v for v in vehicles if v.brand.lower() == brand.lower()]


print("=== Protocols with Attributes ===\n")

# Create vehicles
car = Car("Toyota", "Camry", 2022)
motorcycle = Motorcycle("Harley-Davidson", "Street 750", 2021)
scooter = ElectricScooter("Xiaomi", "Mi Electric", 2023)

vehicles = [car, motorcycle, scooter]

# Test drive each
print("--- Test Drives ---")
for v in vehicles:
    test_drive(v)
    print()

# Summary - accessing protocol attributes
vehicle_summary(vehicles)

# Search by brand
print("\n--- Search by Brand ---")
toyotas = find_by_brand(vehicles, "toyota")
print(f"Toyota vehicles: {[f'{v.model}' for v in toyotas]}")

# Incomplete implementation
print("\n--- Incomplete Implementation ---")

class Bicycle:
    """Bicycle is missing some protocol requirements."""
    
    def __init__(self, brand):
        self.brand = brand
        # Missing: model, year attributes
    
    def start(self) -> str:
        return "Start pedaling"
    
    # Missing: stop(), get_info() methods

bicycle = Bicycle("Trek")
print(f"Bicycle created: {bicycle.brand}")
print("Type checker would flag Bicycle as not satisfying Vehicle protocol")
print("Missing: model, year, stop(), get_info()")

print("\n=== Protocol Attribute Rules ===")
print("""
1. Attribute declarations in Protocol:
   class MyProtocol(Protocol):
       name: str      # Required attribute
       value: int     # Required attribute
       
2. Implementing classes need:
   - Same attribute names
   - Same types (for type checkers)
   
3. Attributes can have default values in implementations
   
4. Protocol just checks structure:
   - Has the attribute? ✓
   - Has the method? ✓
   - Correct types? ✓ (type checker)
""")

Define attributes as class variables with types. Implementers must have them.

Runtime checkable

Make protocols work with isinstance().

runtime_checkable.py
# Runtime Checkable Protocols

from typing import Protocol, runtime_checkable

# Regular protocol (NOT runtime checkable)
class Speakable(Protocol):
    """Protocol without @runtime_checkable."""
    
    def speak(self) -> str:
        ...


# Runtime checkable protocol
@runtime_checkable
class Walkable(Protocol):
    """
    Protocol with @runtime_checkable.
    Can use isinstance() with this protocol.
    """
    
    def walk(self) -> str:
        ...


@runtime_checkable
class Swimmable(Protocol):
    """Another runtime checkable protocol."""
    
    def swim(self) -> str:
        ...


# Classes implementing protocols
class Dog:
    """Dog can walk and speak."""
    
    def walk(self) -> str:
        return "Dog walks on four legs"
    
    def speak(self) -> str:
        return "Woof!"


class Fish:
    """Fish can only swim."""
    
    def swim(self) -> str:
        return "Fish swims with fins"


class Duck:
    """Duck can do everything!"""
    
    def walk(self) -> str:
        return "Duck waddles"
    
    def swim(self) -> str:
        return "Duck paddles on water"
    
    def speak(self) -> str:
        return "Quack!"


class Robot:
    """Robot can walk."""
    
    def walk(self) -> str:
        return "Robot walks mechanically"


print("=== Runtime Checkable Protocols ===\n")

# Create objects
dog = Dog()
fish = Fish()
duck = Duck()
robot = Robot()

objects = [dog, fish, duck, robot]

# Test with runtime checkable protocol
print("--- isinstance() with @runtime_checkable ---")
print("\nWalkable check (has walk() method?):")
for obj in objects:
    name = type(obj).__name__
    is_walkable = isinstance(obj, Walkable)
    print(f"  {name}: isinstance(obj, Walkable) = {is_walkable}")

print("\nSwimmable check (has swim() method?):")
for obj in objects:
    name = type(obj).__name__
    is_swimmable = isinstance(obj, Swimmable)
    print(f"  {name}: isinstance(obj, Swimmable) = {is_swimmable}")

# Test with non-runtime-checkable protocol
print("\n--- isinstance() without @runtime_checkable ---")
try:
    result = isinstance(dog, Speakable)
    print(f"Dog isinstance(Speakable) = {result}")
except TypeError as e:
    print(f"Error: {e}")
    print("Cannot use isinstance() with non-runtime-checkable protocol!")

# Practical use: filtering by capability
print("\n--- Filtering by Capability ---")

def get_walkers(items: list) -> list[Walkable]:
    """Filter items that can walk."""
    return [item for item in items if isinstance(item, Walkable)]

def get_swimmers(items: list) -> list[Swimmable]:
    """Filter items that can swim."""
    return [item for item in items if isinstance(item, Swimmable)]

walkers = get_walkers(objects)
print(f"Walkers: {[type(w).__name__ for w in walkers]}")

swimmers = get_swimmers(objects)
print(f"Swimmers: {[type(s).__name__ for s in swimmers]}")

# Process only matching objects
print("\n--- Process Only Walkers ---")
for obj in objects:
    if isinstance(obj, Walkable):
        print(f"  {type(obj).__name__}: {obj.walk()}")

print("\n--- Process Only Swimmers ---")
for obj in objects:
    if isinstance(obj, Swimmable):
        print(f"  {type(obj).__name__}: {obj.swim()}")

# Combining protocols
print("\n--- Finding Multi-talented (can both walk AND swim) ---")
for obj in objects:
    if isinstance(obj, Walkable) and isinstance(obj, Swimmable):
        name = type(obj).__name__
        print(f"  {name} can:")
        print(f"    - {obj.walk()}")
        print(f"    - {obj.swim()}")

print("\n=== @runtime_checkable Rules ===")
print("""
1. Without @runtime_checkable:
   - Protocol is for static type checking only
   - Cannot use isinstance()

2. With @runtime_checkable:
   - Can use isinstance() at runtime
   - Checks if object has the required methods
   - Does NOT check method signatures

3. Limitations of runtime checking:
   - Only checks method/attribute names exist
   - Does NOT verify return types
   - Does NOT verify parameter types
   - Less strict than static type checking

4. When to use:
   - Need to filter objects by capability
   - Conditional logic based on protocol
   - Building plugin systems
""")

@runtime_checkable decorator enables isinstance() checks.

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

Protocol vs ABC

When to use each approach.

protocol_vs_abc.py
# Protocol vs ABC Comparison

from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable

print("=== Protocol vs ABC ===\n")

# ========== ABC APPROACH ==========
print("--- ABC (Abstract Base Class) ---")

class ShapeABC(ABC):
    """
    Abstract Base Class approach.
    Subclasses MUST inherit from this.
    """
    
    @abstractmethod
    def area(self) -> float:
        """Calculate area."""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Calculate perimeter."""
        pass


class CircleABC(ShapeABC):
    """Circle MUST inherit from ShapeABC."""
    
    PI = 3.14159
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self) -> float:
        return CircleABC.PI * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * CircleABC.PI * self.radius


class RectangleABC(ShapeABC):
    """Rectangle MUST inherit from ShapeABC."""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self) -> float:
        return self.width * self.height
    
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)


# ========== PROTOCOL APPROACH ==========
print("--- Protocol ---")

@runtime_checkable
class ShapeProtocol(Protocol):
    """
    Protocol approach.
    Classes just need matching methods.
    NO inheritance required.
    """
    
    def area(self) -> float:
        ...
    
    def perimeter(self) -> float:
        ...


class CircleProtocol:
    """Circle WITHOUT inheritance - just has the methods."""
    
    PI = 3.14159
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self) -> float:
        return CircleProtocol.PI * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * CircleProtocol.PI * self.radius


class RectangleProtocol:
    """Rectangle WITHOUT inheritance - just has the methods."""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self) -> float:
        return self.width * self.height
    
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)


# ========== COMPARISON ==========
print("--- Comparison ---\n")

# Function that works with ABC
def calculate_abc(shape: ShapeABC) -> None:
    """Takes ShapeABC - MUST be a subclass."""
    print(f"  Area: {shape.area():.2f}")
    print(f"  Perimeter: {shape.perimeter():.2f}")


# Function that works with Protocol
def calculate_protocol(shape: ShapeProtocol) -> None:
    """Takes ShapeProtocol - just needs matching methods."""
    print(f"  Area: {shape.area():.2f}")
    print(f"  Perimeter: {shape.perimeter():.2f}")


# Create instances
circle_abc = CircleABC(5)
rect_abc = RectangleABC(4, 6)

circle_prot = CircleProtocol(5)
rect_prot = RectangleProtocol(4, 6)

# Test ABC approach
print("ABC Shapes:")
print("Circle:")
calculate_abc(circle_abc)
print("Rectangle:")
calculate_abc(rect_abc)

print()

# Test Protocol approach
print("Protocol Shapes:")
print("Circle:")
calculate_protocol(circle_prot)
print("Rectangle:")
calculate_protocol(rect_prot)

print()

# Key difference: inheritance check
print("--- Inheritance Check ---")
print(f"CircleABC is subclass of ShapeABC: {issubclass(CircleABC, ShapeABC)}")
print(f"CircleProtocol is subclass of ShapeProtocol: {isinstance(circle_prot, ShapeProtocol)}")

# ABC enforces at class definition
print("\n--- ABC Enforcement ---")

class IncompleteABC(ShapeABC):
    """ABC enforces implementation at instantiation."""
    
    def area(self) -> float:
        return 0
    # Missing perimeter()!

try:
    incomplete = IncompleteABC()
except TypeError as e:
    print(f"ABC Error: {e}")

# Protocol allows incomplete (type checker catches it)
print("\n--- Protocol (no enforcement at runtime) ---")

class IncompleteProtocol:
    """Protocol doesn't enforce at runtime."""
    
    def area(self) -> float:
        return 0
    # Missing perimeter() - but no error at creation

incomplete_prot = IncompleteProtocol()
print(f"IncompleteProtocol created successfully")
print(f"isinstance check: {isinstance(incomplete_prot, ShapeProtocol)}")

# Cross-compatibility demonstration
print("\n--- Cross-Compatibility ---")
print("Protocol function can accept ABC classes:")
calculate_protocol(circle_abc)
print("(ABC classes satisfy Protocol if they have the methods!)")

print("\n=== When to Use Which? ===")
print("""
Use ABC when:
├─ You need runtime enforcement
├─ You want to provide default implementations
├─ You have a clear inheritance hierarchy
├─ Classes MUST explicitly inherit
└─ Example: Framework base classes, plugin systems

Use Protocol when:
├─ You want structural (duck) typing
├─ Classes from different libraries should work
├─ No inheritance relationship desired
├─ Type hints for existing code without changes
└─ Example: Accepting any "file-like" object

Can use both:
├─ Protocol for type hints (flexible)
├─ ABC for your own implementations (enforced)
""")

ABC: strict inheritance. Protocol: structural compatibility. Both valid.

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

Built-in protocols

Standard library protocols you use every day.

common_protocols.py
# Common Protocol Patterns and Standard Library Protocols

from typing import Protocol, Iterable, Iterator, Callable, Sized, runtime_checkable

print("=== Common Protocol Patterns ===\n")

# ========== ITERABLE PATTERN ==========
print("--- Iterable Pattern ---")

@runtime_checkable
class IterableProtocol(Protocol):
    """Protocol for objects that can be iterated."""
    
    def __iter__(self):
        ...


class NumberRange:
    """Custom iterable - numbers from start to end."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        current = self.start
        while current <= self.end:
            yield current
            current += 1


# Use in for loop
numbers = NumberRange(1, 5)
print(f"NumberRange(1, 5): {list(numbers)}")
print(f"Is Iterable: {isinstance(numbers, IterableProtocol)}")

print()

# ========== CALLABLE PATTERN ==========
print("--- Callable Pattern ---")

@runtime_checkable
class CallableProtocol(Protocol):
    """Protocol for objects that can be called like functions."""
    
    def __call__(self, *args, **kwargs):
        ...


class Multiplier:
    """Callable class - multiplies by a factor."""
    
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, value):
        return value * self.factor


double = Multiplier(2)
triple = Multiplier(3)

print(f"double(5) = {double(5)}")
print(f"triple(5) = {triple(5)}")
print(f"Is Callable: {isinstance(double, CallableProtocol)}")

# Regular functions are also callable
def square(x):
    return x * x

print(f"square is Callable: {isinstance(square, CallableProtocol)}")

print()

# ========== SIZED PATTERN ==========
print("--- Sized Pattern ---")

@runtime_checkable
class SizedProtocol(Protocol):
    """Protocol for objects with length."""
    
    def __len__(self) -> int:
        ...


class Playlist:
    """Sized class - has length."""
    
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add(self, song):
        self.songs.append(song)
    
    def __len__(self) -> int:
        return len(self.songs)


playlist = Playlist("My Mix")
playlist.add("Song A")
playlist.add("Song B")
playlist.add("Song C")

print(f"Playlist '{playlist.name}' has {len(playlist)} songs")
print(f"Is Sized: {isinstance(playlist, SizedProtocol)}")

print()

# ========== CONTAINER PATTERN ==========
print("--- Container Pattern (supports 'in') ---")

@runtime_checkable
class ContainerProtocol(Protocol):
    """Protocol for objects that support 'in' operator."""
    
    def __contains__(self, item) -> bool:
        ...


class ShoppingCart:
    """Container class - supports 'in' operator."""
    
    def __init__(self):
        self.items = []
    
    def add(self, item):
        self.items.append(item)
    
    def __contains__(self, item) -> bool:
        return item in self.items


cart = ShoppingCart()
cart.add("Apple")
cart.add("Banana")

print(f"'Apple' in cart: {'Apple' in cart}")
print(f"'Orange' in cart: {'Orange' in cart}")
print(f"Is Container: {isinstance(cart, ContainerProtocol)}")

print()

# ========== CONTEXT MANAGER PATTERN ==========
print("--- Context Manager Pattern ---")

@runtime_checkable
class ContextManagerProtocol(Protocol):
    """Protocol for objects that work with 'with' statement."""
    
    def __enter__(self):
        ...
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        ...


class Timer:
    """Context manager that measures execution time."""
    
    def __init__(self, name):
        self.name = name
        self.start = 0
    
    def __enter__(self):
        from time import time
        self.start = time()
        print(f"[{self.name}] Starting...")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        from time import time
        elapsed = time() - self.start
        print(f"[{self.name}] Completed in {elapsed:.4f} seconds")
        return False


print(f"Is Context Manager: {isinstance(Timer('test'), ContextManagerProtocol)}")

# Use the context manager
with Timer("Processing") as t:
    total = sum(range(10000))
    print(f"  Sum calculated: {total}")

print()

# ========== COMBINING PROTOCOLS ==========
print("--- Combining Multiple Protocols ---")

class DataStore:
    """Class implementing multiple protocols."""
    
    def __init__(self):
        self.data = []
    
    def add(self, item):
        self.data.append(item)
    
    # Iterable
    def __iter__(self):
        return iter(self.data)
    
    # Sized
    def __len__(self) -> int:
        return len(self.data)
    
    # Container
    def __contains__(self, item) -> bool:
        return item in self.data


store = DataStore()
store.add("A")
store.add("B")
store.add("C")

print(f"DataStore: {list(store)}")
print(f"Length: {len(store)}")
print(f"'B' in store: {'B' in store}")

print("\nProtocol checks:")
print(f"  Is Iterable: {isinstance(store, IterableProtocol)}")
print(f"  Is Sized: {isinstance(store, SizedProtocol)}")
print(f"  Is Container: {isinstance(store, ContainerProtocol)}")

print("\n=== Standard Library Protocols ===")
print("""
typing module provides many protocols:

Iterable[T]     - has __iter__()
Iterator[T]     - has __iter__() and __next__()
Callable[...,R] - can be called
Sized           - has __len__()
Container[T]    - has __contains__()
Hashable        - has __hash__()
Reversible[T]   - has __reversed__()
SupportsInt     - has __int__()
SupportsFloat   - has __float__()
SupportsAbs     - has __abs__()
SupportsBytes   - has __bytes__()

Example usage:
    from typing import Iterable, Callable
    
    def process(items: Iterable[str], func: Callable[[str], str]):
        return [func(item) for item in items]
""")

Iterable, Callable, Sized, Hashable - all protocols.

Exercise: practical.py

Design a file-like protocol for custom storage backends