Your code processes shapes. Each shape has an area() method, but Circle and Rectangle calculate area differently. Polymorphism lets you call shape.area() without knowing which specific shape it is - the right method runs automatically.

Duck typing

If it has the right methods, it works.

duck_typing.py
# Duck Typing - "If it quacks like a duck..."

class Duck:
    """A real duck."""
    
    def quack(self):
        return "Quack quack!"
    
    def walk(self):
        return "Waddle waddle"


class Person:
    """A person who can imitate a duck."""
    
    def quack(self):
        return "I'm pretending to quack!"
    
    def walk(self):
        return "Walking on two legs"


class RobotDuck:
    """A mechanical duck toy."""
    
    def quack(self):
        return "Electronic quack!"
    
    def walk(self):
        return "Mechanical walking..."


class Dog:
    """A dog cannot quack."""
    
    def bark(self):
        return "Woof!"
    
    def walk(self):
        return "Running on four legs"


# Function that uses duck typing
def make_it_quack(duck_like_thing):
    """Works with anything that has quack() method."""
    return duck_like_thing.quack()


def duck_show(duck_like_thing):
    """Demonstrate both quack and walk."""
    print(f"  Quack: {duck_like_thing.quack()}")
    print(f"  Walk: {duck_like_thing.walk()}")


print("=== Duck Typing ===\n")

print("The principle:")
print("'If it walks like a duck and quacks like a duck, it's a duck!'")
print("(Actual type doesn't matter - behavior does)")

# Create objects of different types
real_duck = Duck()
person = Person()
robot = RobotDuck()

print("\n--- Making things quack ---")

# All can quack!
quackers = [real_duck, person, robot]

for obj in quackers:
    print(f"{type(obj).__name__}: {make_it_quack(obj)}")

print("\n--- Full duck demonstration ---")

for obj in quackers:
    print(f"\n{type(obj).__name__}:")
    duck_show(obj)

print("\n--- What about Dog? ---")

dog = Dog()
print(f"Dog can walk: {dog.walk()}")
print("But if we try to make Dog quack...")

try:
    make_it_quack(dog)
except AttributeError as e:
    print(f"Error: {e}")
    print("Dog doesn't have quack() method!")

print("\n=== Duck Typing Rules ===")
print("""
1. No formal interface or inheritance required
2. Object just needs the right methods
3. Type is checked at runtime (not compile time)
4. AttributeError if method missing

Advantages:
- Flexible and dynamic
- Easy to extend
- No type hierarchy needed

Disadvantages:
- Errors only at runtime
- IDE may not catch mistakes
- Need good documentation/naming
""")

No interface declaration needed. If it has speak(), you can call speak().

duck typing "If it quacks like a duck..." - any object with right methods works.

Method override polymorphism

Same method name, different behavior per class.

method_override_poly.py
# Polymorphism Through Method Overriding

class Animal:
    """Base class for animals."""
    
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        """Default speak - to be overridden."""
        return f"{self.name} makes a sound"
    
    def describe(self):
        return f"{self.name} is an animal"


class Dog(Animal):
    """Dog overrides speak."""
    
    def speak(self):
        return f"{self.name} says: Woof woof!"
    
    def describe(self):
        return f"{self.name} is a loyal dog"


class Cat(Animal):
    """Cat overrides speak."""
    
    def speak(self):
        return f"{self.name} says: Meow~"
    
    def describe(self):
        return f"{self.name} is an independent cat"


class Cow(Animal):
    """Cow overrides speak."""
    
    def speak(self):
        return f"{self.name} says: Moo!"
    
    def describe(self):
        return f"{self.name} is a gentle cow"


class Fish(Animal):
    """Fish - some methods make less sense."""
    
    def speak(self):
        return f"{self.name} says: ... (bubbles)"
    
    def describe(self):
        return f"{self.name} is a quiet fish"


# Polymorphic function
def animal_concert(animals):
    """Make all animals speak - polymorphic behavior!"""
    print("** Animal Concert **")
    print("-" * 30)
    for animal in animals:
        print(animal.speak())
    print("-" * 30)


def animal_roll_call(animals):
    """Describe all animals."""
    print("\nRoll Call:")
    for animal in animals:
        print(f"  - {animal.describe()}")


print("=== Polymorphism via Method Overriding ===\n")

# Create different animals
animals = [
    Dog("Buddy"),
    Cat("Whiskers"),
    Cow("Bessie"),
    Fish("Nemo"),
    Dog("Max"),
    Cat("Mittens"),
]

# Same method name, different behavior
animal_concert(animals)

animal_roll_call(animals)

# The beauty of polymorphism
print("\n--- Processing uniformly ---")

for animal in animals:
    # All animals have same interface
    print(f"{animal.name}: ", end="")
    print(animal.speak())

# Adding new animal type - no function changes needed!
print("\n--- Adding new animal type ---")

class Duck(Animal):
    def speak(self):
        return f"{self.name} says: Quack quack!"
    
    def describe(self):
        return f"{self.name} is a happy duck"

# Works immediately with existing functions
duck = Duck("Donald")
print(f"New animal: {duck.describe()}")
print(f"In concert: {duck.speak()}")

# Add to list and process
animals.append(duck)
print(f"\nTotal animals now: {len(animals)}")

print("\n=== Key Points ===")
print("""
1. Parent class defines the method (speak)
2. Each child overrides with specific behavior
3. Functions work with parent type (Animal)
4. Actual behavior depends on runtime type
5. New types work without changing functions

This is the essence of polymorphism:
  - One interface (Animal.speak)
  - Many implementations (Dog, Cat, Cow, etc.)
  - Uniform handling (animal_concert works for all)
""")

Parent defines method. Each child overrides with its own implementation.

Common interface

Design classes to share method names.

common_interface.py
# Designing Classes with Common Interface

class Drawable:
    """
    Classes that can be drawn share this interface.
    In Python, this is just a convention - no formal interface.
    """
    
    def draw(self):
        """Draw the object - override this!"""
        raise NotImplementedError("Subclasses must implement draw()")
    
    def get_bounds(self):
        """Return bounding box - override this!"""
        raise NotImplementedError("Subclasses must implement get_bounds()")


class Circle:
    """Circle implements Drawable interface."""
    
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    
    def draw(self):
        return f"Drawing circle at ({self.x}, {self.y}) with radius {self.radius}"
    
    def get_bounds(self):
        return (self.x - self.radius, self.y - self.radius,
                self.x + self.radius, self.y + self.radius)


class Rectangle:
    """Rectangle implements Drawable interface."""
    
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
    
    def draw(self):
        return f"Drawing rectangle at ({self.x}, {self.y}), size {self.width}x{self.height}"
    
    def get_bounds(self):
        return (self.x, self.y, self.x + self.width, self.y + self.height)


class Line:
    """Line implements Drawable interface."""
    
    def __init__(self, x1, y1, x2, y2):
        self.x1, self.y1 = x1, y1
        self.x2, self.y2 = x2, y2
    
    def draw(self):
        return f"Drawing line from ({self.x1}, {self.y1}) to ({self.x2}, {self.y2})"
    
    def get_bounds(self):
        return (min(self.x1, self.x2), min(self.y1, self.y2),
                max(self.x1, self.x2), max(self.y1, self.y2))


class Text:
    """Text implements Drawable interface."""
    
    def __init__(self, x, y, content, font_size=12):
        self.x = x
        self.y = y
        self.content = content
        self.font_size = font_size
    
    def draw(self):
        return f"Drawing text '{self.content}' at ({self.x}, {self.y}), size {self.font_size}"
    
    def get_bounds(self):
        # Approximate bounds based on content length
        width = len(self.content) * self.font_size * 0.6
        height = self.font_size
        return (self.x, self.y, self.x + width, self.y + height)


# Canvas that uses the common interface
class Canvas:
    """Canvas that holds and renders drawable objects."""
    
    def __init__(self, name):
        self.name = name
        self.objects = []
    
    def add(self, drawable):
        """Add any object with draw() method."""
        self.objects.append(drawable)
    
    def render_all(self):
        """Render all objects - polymorphic!"""
        print(f"\n🖼️ Rendering {self.name}:")
        print("-" * 40)
        for obj in self.objects:
            print(f"  {obj.draw()}")
        print("-" * 40)
    
    def get_total_bounds(self):
        """Calculate bounding box for all objects."""
        if not self.objects:
            return (0, 0, 0, 0)
        
        bounds_list = [obj.get_bounds() for obj in self.objects]
        min_x = min(b[0] for b in bounds_list)
        min_y = min(b[1] for b in bounds_list)
        max_x = max(b[2] for b in bounds_list)
        max_y = max(b[3] for b in bounds_list)
        
        return (min_x, min_y, max_x, max_y)


print("=== Common Interface Design ===\n")

# Create drawable objects
circle = Circle(100, 100, 50)
rect = Rectangle(200, 50, 80, 60)
line = Line(0, 0, 150, 150)
text = Text(50, 200, "Hello World", 14)

# Create canvas and add objects
canvas = Canvas("My Drawing")
canvas.add(circle)
canvas.add(rect)
canvas.add(line)
canvas.add(text)

# Render - polymorphism in action!
canvas.render_all()

# Get bounds - also polymorphic
total_bounds = canvas.get_total_bounds()
print(f"\nTotal canvas bounds: {total_bounds}")

# Demonstrate individual bounds
print("\nIndividual bounds:")
for obj in canvas.objects:
    print(f"  {type(obj).__name__}: {obj.get_bounds()}")

# Add more objects dynamically
print("\n--- Adding more objects ---")
canvas.add(Circle(300, 300, 25))
canvas.add(Text(10, 10, "New!", 20))

canvas.render_all()

print("\n=== Interface Design Best Practices ===")
print("""
1. Define clear method signatures
   - draw() for all drawable objects
   - get_bounds() for spatial info

2. Document the expected interface
   - Use docstrings
   - Consider type hints

3. Use NotImplementedError for base class
   - Signals "must override"
   - Fails fast if forgotten

4. Keep interface minimal
   - Only essential methods
   - Easy to implement

5. Test with duck typing
   - If it has draw() and get_bounds()
   - It works with Canvas!
""")

All shapes have area() and perimeter(). Code works with any shape.

interface Set of methods a class provides. In Python, implicit through duck typing.

Polymorphic functions

Write functions that work with any compatible object.

polymorphic_functions.py
# Polymorphic Functions

class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Woof!"
    
    def __len__(self):
        return len(self.name)


class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Meow!"
    
    def __len__(self):
        return len(self.name)


class Robot:
    def __init__(self, model):
        self.model = model
    
    def speak(self):
        return "Beep boop!"
    
    def __len__(self):
        return len(self.model)


# Polymorphic function - works with any object that has speak()
def make_speak(speaker):
    """
    Works with any object that has a speak() method.
    This is polymorphism via duck typing.
    """
    return speaker.speak()


# Function with type-specific logic
def describe_speaker(speaker):
    """
    Polymorphic function that also handles type differences.
    """
    sound = speaker.speak()
    
    # Can use hasattr to check for attributes
    if hasattr(speaker, 'name'):
        identifier = speaker.name
    elif hasattr(speaker, 'model'):
        identifier = speaker.model
    else:
        identifier = "Unknown"
    
    return f"{identifier} says: {sound}"


# Function using len() - built-in polymorphism
def show_length(obj):
    """
    len() is polymorphic - works with many types.
    Requires __len__ method.
    """
    return f"Length of {type(obj).__name__}: {len(obj)}"


# Function accepting multiple types with shared behavior
def make_chorus(speakers, times=2):
    """
    Make all speakers speak multiple times.
    Works with any list of objects with speak() method.
    """
    print("\n** Chorus Time! **")
    for _ in range(times):
        for speaker in speakers:
            print(f"  {make_speak(speaker)}")
        print("---")


# Higher-order polymorphic function
def process_speakers(speakers, processor):
    """
    Apply any processor function to speakers.
    Both the speakers and processor are polymorphic!
    """
    results = []
    for speaker in speakers:
        result = processor(speaker)
        results.append(result)
    return results


print("=== Polymorphic Functions ===\n")

# Create different objects
dog = Dog("Buddy")
cat = Cat("Whiskers")
robot = Robot("R2D2")

speakers = 

# Basic polymorphic function
print("--- make_speak (basic polymorphism) ---")
for s in speakers:
    print(f"  {make_speak(s)}")

# Polymorphic with type handling
print("\n--- describe_speaker (with type handling) ---")
for s in speakers:
    print(f"  {describe_speaker(s)}")

# Built-in len() polymorphism
print("\n--- show_length (built-in len polymorphism) ---")
for s in speakers:
    print(f"  {show_length(s)}")

# Also works with built-in types!
print("\n  Also works with built-in types:")
print(f"  {show_length('Hello')}")
print(f"  {show_length([1, 2, 3])}")
print(f"  {show_length({'a': 1, 'b': 2})}")

# Chorus function
make_chorus(speakers)

# Higher-order function
print("\n--- process_speakers (higher-order) ---")

# Different processors
def loud_speak(s):
    return s.speak().upper() + "!!"

def count_speak(s):
    return f"{s.speak()} ({len(s.speak())} chars)"

print("Loud processor:")
results = process_speakers(speakers, loud_speak)
for r in results:
    print(f"  {r}")

print("\nCount processor:")
results = process_speakers(speakers, count_speak)
for r in results:
    print(f"  {r}")

print("\n=== Polymorphic Function Benefits ===")
print("""
1. Reusable: One function works with many types
2. Flexible: Easy to add new types
3. Clean: No type-checking spaghetti
4. Pythonic: Embraces duck typing

Best practices:
- Document expected interface
- Use meaningful parameter names
- Handle missing methods gracefully (hasattr)
- Consider type hints for documentation
""")

# Polymorphic Functions

class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Woof!"
    
    def __len__(self):
        return len(self.name)


class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Meow!"
    
    def __len__(self):
        return len(self.name)


class Robot:
    def __init__(self, model):
        self.model = model
    
    def speak(self):
        return "Beep boop!"
    
    def __len__(self):
        return len(self.model)


# Polymorphic function - works with any object that has speak()
def make_speak(speaker):
    """
    Works with any object that has a speak() method.
    This is polymorphism via duck typing.
    """
    return speaker.speak()


# Function with type-specific logic
def describe_speaker(speaker):
    """
    Polymorphic function that also handles type differences.
    """
    sound = speaker.speak()
    
    # Can use hasattr to check for attributes
    if hasattr(speaker, 'name'):
        identifier = speaker.name
    elif hasattr(speaker, 'model'):
        identifier = speaker.model
    else:
        identifier = "Unknown"
    
    return f"{identifier} says: {sound}"


# Function using len() - built-in polymorphism
def show_length(obj):
    """
    len() is polymorphic - works with many types.
    Requires __len__ method.
    """
    return f"Length of {type(obj).__name__}: {len(obj)}"


# Function accepting multiple types with shared behavior
def make_chorus(speakers, times=2):
    """
    Make all speakers speak multiple times.
    Works with any list of objects with speak() method.
    """
    print("\n** Chorus Time! **")
    for _ in range(times):
        for speaker in speakers:
            print(f"  {make_speak(speaker)}")
        print("---")


# Higher-order polymorphic function
def process_speakers(speakers, processor):
    """
    Apply any processor function to speakers.
    Both the speakers and processor are polymorphic!
    """
    results = []
    for speaker in speakers:
        result = processor(speaker)
        results.append(result)
    return results


print("=== Polymorphic Functions ===\n")

# Create different objects
dog = Dog("Buddy")
cat = Cat("Whiskers")
robot = Robot("R2D2")

speakers = 

# Basic polymorphic function
print("--- make_speak (basic polymorphism) ---")
for s in speakers:
    print(f"  {make_speak(s)}")

# Polymorphic with type handling
print("\n--- describe_speaker (with type handling) ---")
for s in speakers:
    print(f"  {describe_speaker(s)}")

# Built-in len() polymorphism
print("\n--- show_length (built-in len polymorphism) ---")
for s in speakers:
    print(f"  {show_length(s)}")

# Also works with built-in types!
print("\n  Also works with built-in types:")
print(f"  {show_length('Hello')}")
print(f"  {show_length([1, 2, 3])}")
print(f"  {show_length({'a': 1, 'b': 2})}")

# Chorus function
make_chorus(speakers)

# Higher-order function
print("\n--- process_speakers (higher-order) ---")

# Different processors
def loud_speak(s):
    return s.speak().upper() + "!!"

def count_speak(s):
    return f"{s.speak()} ({len(s.speak())} chars)"

print("Loud processor:")
results = process_speakers(speakers, loud_speak)
for r in results:
    print(f"  {r}")

print("\nCount processor:")
results = process_speakers(speakers, count_speak)
for r in results:
    print(f"  {r}")

print("\n=== Polymorphic Function Benefits ===")
print("""
1. Reusable: One function works with many types
2. Flexible: Easy to add new types
3. Clean: No type-checking spaghetti
4. Pythonic: Embraces duck typing

Best practices:
- Document expected interface
- Use meaningful parameter names
- Handle missing methods gracefully (hasattr)
- Consider type hints for documentation
""")

# Polymorphic Functions

class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Woof!"
    
    def __len__(self):
        return len(self.name)


class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Meow!"
    
    def __len__(self):
        return len(self.name)


class Robot:
    def __init__(self, model):
        self.model = model
    
    def speak(self):
        return "Beep boop!"
    
    def __len__(self):
        return len(self.model)


# Polymorphic function - works with any object that has speak()
def make_speak(speaker):
    """
    Works with any object that has a speak() method.
    This is polymorphism via duck typing.
    """
    return speaker.speak()


# Function with type-specific logic
def describe_speaker(speaker):
    """
    Polymorphic function that also handles type differences.
    """
    sound = speaker.speak()
    
    # Can use hasattr to check for attributes
    if hasattr(speaker, 'name'):
        identifier = speaker.name
    elif hasattr(speaker, 'model'):
        identifier = speaker.model
    else:
        identifier = "Unknown"
    
    return f"{identifier} says: {sound}"


# Function using len() - built-in polymorphism
def show_length(obj):
    """
    len() is polymorphic - works with many types.
    Requires __len__ method.
    """
    return f"Length of {type(obj).__name__}: {len(obj)}"


# Function accepting multiple types with shared behavior
def make_chorus(speakers, times=2):
    """
    Make all speakers speak multiple times.
    Works with any list of objects with speak() method.
    """
    print("\n** Chorus Time! **")
    for _ in range(times):
        for speaker in speakers:
            print(f"  {make_speak(speaker)}")
        print("---")


# Higher-order polymorphic function
def process_speakers(speakers, processor):
    """
    Apply any processor function to speakers.
    Both the speakers and processor are polymorphic!
    """
    results = []
    for speaker in speakers:
        result = processor(speaker)
        results.append(result)
    return results


print("=== Polymorphic Functions ===\n")

# Create different objects
dog = Dog("Buddy")
cat = Cat("Whiskers")
robot = Robot("R2D2")

speakers = 

# Basic polymorphic function
print("--- make_speak (basic polymorphism) ---")
for s in speakers:
    print(f"  {make_speak(s)}")

# Polymorphic with type handling
print("\n--- describe_speaker (with type handling) ---")
for s in speakers:
    print(f"  {describe_speaker(s)}")

# Built-in len() polymorphism
print("\n--- show_length (built-in len polymorphism) ---")
for s in speakers:
    print(f"  {show_length(s)}")

# Also works with built-in types!
print("\n  Also works with built-in types:")
print(f"  {show_length('Hello')}")
print(f"  {show_length([1, 2, 3])}")
print(f"  {show_length({'a': 1, 'b': 2})}")

# Chorus function
make_chorus(speakers)

# Higher-order function
print("\n--- process_speakers (higher-order) ---")

# Different processors
def loud_speak(s):
    return s.speak().upper() + "!!"

def count_speak(s):
    return f"{s.speak()} ({len(s.speak())} chars)"

print("Loud processor:")
results = process_speakers(speakers, loud_speak)
for r in results:
    print(f"  {r}")

print("\nCount processor:")
results = process_speakers(speakers, count_speak)
for r in results:
    print(f"  {r}")

print("\n=== Polymorphic Function Benefits ===")
print("""
1. Reusable: One function works with many types
2. Flexible: Easy to add new types
3. Clean: No type-checking spaghetti
4. Pythonic: Embraces duck typing

Best practices:
- Document expected interface
- Use meaningful parameter names
- Handle missing methods gracefully (hasattr)
- Consider type hints for documentation
""")

Function accepts anything with required methods. Very flexible.

Operator overloading

Polymorphism for operators like +, -, ==.

operator_overload.py
# Polymorphism via Operator Overloading

class Vector:
    """2D Vector with operator overloading."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, other):
        """v1 + v2"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        """v1 - v2"""
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        """v * scalar"""
        return Vector(self.x * scalar, self.y * scalar)
    
    def __rmul__(self, scalar):
        """scalar * v (reversed)"""
        return self.__mul__(scalar)
    
    def __eq__(self, other):
        """v1 == v2"""
        return self.x == other.x and self.y == other.y
    
    def __abs__(self):
        """abs(v) - magnitude"""
        return (self.x ** 2 + self.y ** 2) ** 0.5


class Money:
    """Money class with operator overloading."""
    
    def __init__(self, dollars, cents=0):
        total_cents = dollars * 100 + cents
        self.dollars = total_cents // 100
        self.cents = total_cents % 100
    
    def __repr__(self):
        return f"Money({self.dollars}, {self.cents})"
    
    def __str__(self):
        return f"${self.dollars}.{self.cents:02d}"
    
    def __add__(self, other):
        """m1 + m2"""
        total_cents = (self.dollars * 100 + self.cents + 
                      other.dollars * 100 + other.cents)
        return Money(total_cents // 100, total_cents % 100)
    
    def __sub__(self, other):
        """m1 - m2"""
        total_cents = (self.dollars * 100 + self.cents - 
                      other.dollars * 100 - other.cents)
        return Money(total_cents // 100, total_cents % 100)
    
    def __mul__(self, factor):
        """m * factor"""
        total_cents = int((self.dollars * 100 + self.cents) * factor)
        return Money(total_cents // 100, total_cents % 100)
    
    def __rmul__(self, factor):
        return self.__mul__(factor)
    
    def __lt__(self, other):
        """m1 < m2"""
        return (self.dollars * 100 + self.cents) < (other.dollars * 100 + other.cents)
    
    def __le__(self, other):
        """m1 <= m2"""
        return (self.dollars * 100 + self.cents) <= (other.dollars * 100 + other.cents)
    
    def __eq__(self, other):
        """m1 == m2"""
        return self.dollars == other.dollars and self.cents == other.cents


class StringList:
    """List-like class with operator overloading."""
    
    def __init__(self, *items):
        self.items = list(items)
    
    def __repr__(self):
        return f"StringList{tuple(self.items)}"
    
    def __add__(self, other):
        """Concatenate two StringLists"""
        return StringList(*(self.items + other.items))
    
    def __len__(self):
        """len(sl)"""
        return len(self.items)
    
    def __getitem__(self, index):
        """sl[index]"""
        return self.items[index]
    
    def __contains__(self, item):
        """item in sl"""
        return item in self.items
    
    def __iter__(self):
        """for item in sl"""
        return iter(self.items)


print("=== Operator Overloading ===\n")

# Vector operations
print("--- Vector Operations ---")
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1 = {v1}")
print(f"v2 = {v2}")

print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 2 = {v1 * 2}")
print(f"3 * v2 = {3 * v2}")
print(f"|v1| = {abs(v1):.2f}")
print(f"v1 == v2: {v1 == v2}")
print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}")

# Money operations
print("\n--- Money Operations ---")
price = Money(19, 99)
tax = Money(1, 60)
discount = Money(5, 0)

print(f"Price: {price}")
print(f"Tax: {tax}")
print(f"Discount: {discount}")

total = price + tax
print(f"Price + Tax = {total}")

final = total - discount
print(f"After discount = {final}")

doubled = price * 2
print(f"Price * 2 = {doubled}")

print(f"Tax < Discount: {tax < discount}")
print(f"Price == Money(19, 99): {price == Money(19, 99)}")

# StringList operations
print("\n--- StringList Operations ---")
fruits = StringList("apple", "banana")
veggies = StringList("carrot", "broccoli")

print(f"fruits = {fruits}")
print(f"veggies = {veggies}")

combined = fruits + veggies
print(f"combined = {combined}")

print(f"len(combined) = {len(combined)}")
print(f"combined[0] = {combined[0]}")
print(f"'apple' in combined: {'apple' in combined}")

print("Iterating:")
for item in combined:
    print(f"  - {item}")

print("\n=== Common Operators ===")
print("""
Arithmetic:
  __add__(self, other)    +
  __sub__(self, other)    -
  __mul__(self, other)    *
  __truediv__(self, other) /
  __rmul__(self, other)   reversed * (scalar * obj)

Comparison:
  __eq__(self, other)     ==
  __ne__(self, other)     !=
  __lt__(self, other)     <
  __le__(self, other)     <=
  __gt__(self, other)     >
  __ge__(self, other)     >=

Container:
  __len__(self)           len(obj)
  __getitem__(self, key)  obj[key]
  __contains__(self, item) item in obj
  __iter__(self)          for x in obj

Other:
  __repr__(self)          repr(obj), print in shell
  __str__(self)           str(obj), print()
  __abs__(self)           abs(obj)
  __bool__(self)          bool(obj), if obj
""")

Define __add__, __eq__, etc. to make operators work with your class.

operator overloading Define special methods to customize operator behavior for your class.

Exercise: practical.py

Build a payment system with polymorphic processors