Your Shape class defines area() but Shape itself can't compute an area - only Circle and Rectangle can. Abstract classes define methods that subclasses must implement. You can't instantiate an abstract class directly.

Basic ABC

Create an abstract base class.

basic_abc.py
# Basic Abstract Classes with ABC

from abc import ABC, abstractmethod

# Abstract class - cannot be instantiated
class Animal(ABC):
    """
    Abstract base class for animals.
    ABC = Abstract Base Class
    """
    
    def __init__(self, name):
        self.name = name
    
    @abstractmethod
    def speak(self):
        """All animals must implement speak()."""
        pass
    
    @abstractmethod
    def move(self):
        """All animals must implement move()."""
        pass


# Concrete class - implements all abstract methods
class Dog(Animal):
    """Dog implements all abstract methods."""
    
    def speak(self):
        return f"{self.name} says: Woof!"
    
    def move(self):
        return f"{self.name} runs on four legs"


class Cat(Animal):
    """Cat implements all abstract methods."""
    
    def speak(self):
        return f"{self.name} says: Meow!"
    
    def move(self):
        return f"{self.name} walks gracefully"


class Bird(Animal):
    """Bird implements all abstract methods."""
    
    def speak(self):
        return f"{self.name} says: Tweet!"
    
    def move(self):
        return f"{self.name} flies through the air"


print("=== Basic Abstract Classes ===\n")

# Creating concrete instances
dog = Dog("Buddy")
cat = Cat("Whiskers")
bird = Bird("Tweety")

# Use them normally
animals = [dog, cat, bird]

for animal in animals:
    print(f"{type(animal).__name__}:")
    print(f"  {animal.speak()}")
    print(f"  {animal.move()}")
    print()

# Try to instantiate abstract class
print("--- Trying to instantiate abstract class ---")
try:
    animal = Animal("Generic")
except TypeError as e:
    print(f"Error: {e}")
    print("Cannot instantiate abstract class!")

print("\n=== Key Points ===")
print("""
1. Import: from abc import ABC, abstractmethod
2. class MyClass(ABC): makes it abstract
3. @abstractmethod marks methods that MUST be overridden
4. Cannot create instance of abstract class
5. Subclass must implement ALL abstract methods
6. If subclass doesn't implement all, it's also abstract
""")

# Demonstrate incomplete implementation
print("\n--- Incomplete implementation ---")

class Fish(Animal):
    """Fish only implements speak - incomplete!"""
    
    def speak(self):
        return f"{self.name} says: Blub!"
    
    # Missing move() method!

try:
    fish = Fish("Nemo")
except TypeError as e:
    print(f"Error: {e}")
    print("Fish is still abstract because move() is missing!")

from abc import ABC, abstractmethod. Class inherits from ABC.

ABC Abstract Base Class. Can't be instantiated. May have abstract methods.

Abstract methods

Methods that subclasses must implement.

abstract_methods.py
# Defining and Implementing Abstract Methods

from abc import ABC, abstractmethod

# Abstract class with multiple abstract methods
class Shape(ABC):
    """
    Abstract shape class.
    Defines what all shapes must do.
    """
    
    @abstractmethod
    def area(self) -> float:
        """Calculate and return the area."""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Calculate and return the perimeter."""
        pass
    
    @abstractmethod
    def name(self) -> str:
        """Return the name of the shape."""
        pass


class Circle(Shape):
    """Concrete implementation of Shape for circles."""
    
    PI = 3.14159
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self) -> float:
        return Circle.PI * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * Circle.PI * self.radius
    
    def name(self) -> str:
        return "Circle"


class Rectangle(Shape):
    """Concrete implementation of Shape for rectangles."""
    
    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)
    
    def name(self) -> str:
        return "Rectangle"


class Triangle(Shape):
    """Concrete implementation for triangles."""
    
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
    
    def area(self) -> float:
        # Heron's formula
        s = (self.a + self.b + self.c) / 2
        return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
    
    def perimeter(self) -> float:
        return self.a + self.b + self.c
    
    def name(self) -> str:
        return "Triangle"


# Utility function that works with any Shape
def describe_shape(shape: Shape):
    """Works with any Shape - relies on abstract methods."""
    print(f"Shape: {shape.name()}")
    print(f"  Area: {shape.area():.2f}")
    print(f"  Perimeter: {shape.perimeter():.2f}")


def total_area(shapes: list) -> float:
    """Calculate total area of all shapes."""
    return sum(shape.area() for shape in shapes)


print("=== Abstract Methods ===\n")

# Create shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(3, 4, 5)

shapes = 

# Describe each shape
for shape in shapes:
    describe_shape(shape)
    print()

# Calculate total area
total = total_area(shapes)
print(f"Total area of all shapes: {total:.2f}")

# Abstract method signature documentation
print("\n--- Method Signatures ---")
print("Shape abstract methods define the interface:")
print(f"  area() -> float")
print(f"  perimeter() -> float")
print(f"  name() -> str")

# Type checking with isinstance
print("\n--- Type Checking ---")
for shape in shapes:
    print(f"{shape.name()} is a Shape: {isinstance(shape, Shape)}")

print("\n=== Why Abstract Methods? ===")
print("""
1. Enforce Contract:
   - Subclasses MUST implement these methods
   - Compile-time-like safety at class definition

2. Document Interface:
   - Clear what methods are required
   - Type hints show expected return types

3. Enable Polymorphism:
   - Functions can depend on abstract methods
   - Works with any concrete implementation

4. Design Patterns:
   - Strategy pattern
   - Template method pattern
   - Factory pattern
""")

# Defining and Implementing Abstract Methods

from abc import ABC, abstractmethod

# Abstract class with multiple abstract methods
class Shape(ABC):
    """
    Abstract shape class.
    Defines what all shapes must do.
    """
    
    @abstractmethod
    def area(self) -> float:
        """Calculate and return the area."""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Calculate and return the perimeter."""
        pass
    
    @abstractmethod
    def name(self) -> str:
        """Return the name of the shape."""
        pass


class Circle(Shape):
    """Concrete implementation of Shape for circles."""
    
    PI = 3.14159
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self) -> float:
        return Circle.PI * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * Circle.PI * self.radius
    
    def name(self) -> str:
        return "Circle"


class Rectangle(Shape):
    """Concrete implementation of Shape for rectangles."""
    
    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)
    
    def name(self) -> str:
        return "Rectangle"


class Triangle(Shape):
    """Concrete implementation for triangles."""
    
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
    
    def area(self) -> float:
        # Heron's formula
        s = (self.a + self.b + self.c) / 2
        return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
    
    def perimeter(self) -> float:
        return self.a + self.b + self.c
    
    def name(self) -> str:
        return "Triangle"


# Utility function that works with any Shape
def describe_shape(shape: Shape):
    """Works with any Shape - relies on abstract methods."""
    print(f"Shape: {shape.name()}")
    print(f"  Area: {shape.area():.2f}")
    print(f"  Perimeter: {shape.perimeter():.2f}")


def total_area(shapes: list) -> float:
    """Calculate total area of all shapes."""
    return sum(shape.area() for shape in shapes)


print("=== Abstract Methods ===\n")

# Create shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(3, 4, 5)

shapes = 

# Describe each shape
for shape in shapes:
    describe_shape(shape)
    print()

# Calculate total area
total = total_area(shapes)
print(f"Total area of all shapes: {total:.2f}")

# Abstract method signature documentation
print("\n--- Method Signatures ---")
print("Shape abstract methods define the interface:")
print(f"  area() -> float")
print(f"  perimeter() -> float")
print(f"  name() -> str")

# Type checking with isinstance
print("\n--- Type Checking ---")
for shape in shapes:
    print(f"{shape.name()} is a Shape: {isinstance(shape, Shape)}")

print("\n=== Why Abstract Methods? ===")
print("""
1. Enforce Contract:
   - Subclasses MUST implement these methods
   - Compile-time-like safety at class definition

2. Document Interface:
   - Clear what methods are required
   - Type hints show expected return types

3. Enable Polymorphism:
   - Functions can depend on abstract methods
   - Works with any concrete implementation

4. Design Patterns:
   - Strategy pattern
   - Template method pattern
   - Factory pattern
""")

# Defining and Implementing Abstract Methods

from abc import ABC, abstractmethod

# Abstract class with multiple abstract methods
class Shape(ABC):
    """
    Abstract shape class.
    Defines what all shapes must do.
    """
    
    @abstractmethod
    def area(self) -> float:
        """Calculate and return the area."""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Calculate and return the perimeter."""
        pass
    
    @abstractmethod
    def name(self) -> str:
        """Return the name of the shape."""
        pass


class Circle(Shape):
    """Concrete implementation of Shape for circles."""
    
    PI = 3.14159
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self) -> float:
        return Circle.PI * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * Circle.PI * self.radius
    
    def name(self) -> str:
        return "Circle"


class Rectangle(Shape):
    """Concrete implementation of Shape for rectangles."""
    
    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)
    
    def name(self) -> str:
        return "Rectangle"


class Triangle(Shape):
    """Concrete implementation for triangles."""
    
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
    
    def area(self) -> float:
        # Heron's formula
        s = (self.a + self.b + self.c) / 2
        return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
    
    def perimeter(self) -> float:
        return self.a + self.b + self.c
    
    def name(self) -> str:
        return "Triangle"


# Utility function that works with any Shape
def describe_shape(shape: Shape):
    """Works with any Shape - relies on abstract methods."""
    print(f"Shape: {shape.name()}")
    print(f"  Area: {shape.area():.2f}")
    print(f"  Perimeter: {shape.perimeter():.2f}")


def total_area(shapes: list) -> float:
    """Calculate total area of all shapes."""
    return sum(shape.area() for shape in shapes)


print("=== Abstract Methods ===\n")

# Create shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(3, 4, 5)

shapes = 

# Describe each shape
for shape in shapes:
    describe_shape(shape)
    print()

# Calculate total area
total = total_area(shapes)
print(f"Total area of all shapes: {total:.2f}")

# Abstract method signature documentation
print("\n--- Method Signatures ---")
print("Shape abstract methods define the interface:")
print(f"  area() -> float")
print(f"  perimeter() -> float")
print(f"  name() -> str")

# Type checking with isinstance
print("\n--- Type Checking ---")
for shape in shapes:
    print(f"{shape.name()} is a Shape: {isinstance(shape, Shape)}")

print("\n=== Why Abstract Methods? ===")
print("""
1. Enforce Contract:
   - Subclasses MUST implement these methods
   - Compile-time-like safety at class definition

2. Document Interface:
   - Clear what methods are required
   - Type hints show expected return types

3. Enable Polymorphism:
   - Functions can depend on abstract methods
   - Works with any concrete implementation

4. Design Patterns:
   - Strategy pattern
   - Template method pattern
   - Factory pattern
""")

@abstractmethod decorator. Subclass must override or it's also abstract.

abstractmethod Method without implementation. Subclass must provide the implementation.

Concrete methods in ABC

Mix abstract and regular methods.

concrete_methods.py
# Mixing Abstract and Concrete Methods

from abc import ABC, abstractmethod

class DataProcessor(ABC):
    """
    Abstract class with both abstract and concrete methods.
    Concrete methods provide reusable functionality.
    """
    
    def __init__(self, name):
        self.name = name
        self.processed_count = 0
    
    # Abstract methods - MUST be implemented
    @abstractmethod
    def validate(self, data) -> bool:
        """Validate the data - implementation varies by type."""
        pass
    
    @abstractmethod
    def transform(self, data):
        """Transform the data - implementation varies by type."""
        pass
    
    @abstractmethod
    def save(self, data):
        """Save the transformed data - implementation varies."""
        pass
    
    # Concrete methods - inherited by all subclasses
    def process(self, data):
        """
        Template method pattern - concrete method using abstract methods.
        This orchestrates the processing workflow.
        """
        print(f"[{self.name}] Starting processing...")
        
        # Step 1: Validate
        if not self.validate(data):
            print(f"[{self.name}] Validation failed!")
            return None
        print(f"[{self.name}] Validation passed")
        
        # Step 2: Transform
        result = self.transform(data)
        print(f"[{self.name}] Transformation complete")
        
        # Step 3: Save
        self.save(result)
        print(f"[{self.name}] Data saved")
        
        # Step 4: Update stats (concrete)
        self.processed_count += 1
        
        return result
    
    def get_stats(self):
        """Concrete method - shared by all processors."""
        return f"{self.name}: processed {self.processed_count} items"


class JSONProcessor(DataProcessor):
    """Processes JSON-like data (dictionaries)."""
    
    def __init__(self):
        super().__init__("JSONProcessor")
        self.storage = []
    
    def validate(self, data) -> bool:
        """JSON validation - must be dict with 'id' field."""
        return isinstance(data, dict) and 'id' in data
    
    def transform(self, data):
        """Add timestamp to JSON data."""
        from datetime import datetime
        transformed = data.copy()
        transformed['processed_at'] = datetime.now().isoformat()
        return transformed
    
    def save(self, data):
        """Save to internal storage."""
        self.storage.append(data)


class CSVProcessor(DataProcessor):
    """Processes CSV-like data (lists of values)."""
    
    def __init__(self):
        super().__init__("CSVProcessor")
        self.storage = []
    
    def validate(self, data) -> bool:
        """CSV validation - must be list with at least 2 items."""
        return isinstance(data, list) and len(data) >= 2
    
    def transform(self, data):
        """Convert all values to strings and strip whitespace."""
        return [str(item).strip() for item in data]
    
    def save(self, data):
        """Save to storage as comma-separated string."""
        self.storage.append(','.join(data))


class TextProcessor(DataProcessor):
    """Processes plain text data."""
    
    def __init__(self):
        super().__init__("TextProcessor")
        self.storage = []
    
    def validate(self, data) -> bool:
        """Text validation - must be non-empty string."""
        return isinstance(data, str) and len(data.strip()) > 0
    
    def transform(self, data):
        """Normalize text - strip and uppercase."""
        return data.strip().upper()
    
    def save(self, data):
        """Save to storage."""
        self.storage.append(data)


print("=== Abstract and Concrete Methods ===\n")

# Create processors
json_proc = JSONProcessor()
csv_proc = CSVProcessor()
text_proc = TextProcessor()

# Process JSON data
print("--- JSON Processing ---")
json_data = {"id": 1, "name": "Alice", "email": "alice@example.com"}
result = json_proc.process(json_data)
print(f"Result: {result}")

print()

# Process CSV data
print("--- CSV Processing ---")
csv_data = ["  John  ", "  Doe  ", " john@example.com "]
result = csv_proc.process(csv_data)
print(f"Result: {result}")

print()

# Process Text data
print("--- Text Processing ---")
text_data = "  hello world  "
result = text_proc.process(text_data)
print(f"Result: '{result}'")

# Test validation failure
print("\n--- Validation Failure ---")
bad_json = {"name": "No ID field"}
result = json_proc.process(bad_json)
print(f"Result: {result}")

# Stats using concrete method
print("\n--- Stats (Concrete Method) ---")
print(json_proc.get_stats())
print(csv_proc.get_stats())
print(text_proc.get_stats())

print("\n=== Template Method Pattern ===")
print("""
The process() method is a "Template Method":
  1. It's concrete - defined in abstract class
  2. It calls abstract methods (validate, transform, save)
  3. It defines the workflow/algorithm skeleton
  4. Subclasses customize by implementing abstract methods

Benefits:
  - Code reuse (workflow logic is shared)
  - Consistent behavior (all processors follow same steps)
  - Easy to extend (just implement 3 methods)
  - DRY principle (Don't Repeat Yourself)
""")

ABC can have implemented methods that subclasses inherit.

Abstract properties

Properties that must be implemented.

abstract_properties.py
# Abstract Properties

from abc import ABC, abstractmethod

class Vehicle(ABC):
    """
    Abstract class with abstract properties.
    Properties define read-only or read-write attributes.
    """
    
    def __init__(self, brand):
        self._brand = brand
    
    # Abstract property - must be implemented
    @property
    @abstractmethod
    def wheels(self) -> int:
        """Number of wheels - varies by vehicle type."""
        pass
    
    @property
    @abstractmethod
    def vehicle_type(self) -> str:
        """Type of vehicle."""
        pass
    
    # Concrete property - shared by all
    @property
    def brand(self) -> str:
        """Brand is concrete - same logic for all."""
        return self._brand
    
    # Concrete method using abstract properties
    def describe(self) -> str:
        return f"{self.brand} {self.vehicle_type} with {self.wheels} wheels"


class Car(Vehicle):
    """Car implementation."""
    
    @property
    def wheels(self) -> int:
        return 4
    
    @property
    def vehicle_type(self) -> str:
        return "Car"


class Motorcycle(Vehicle):
    """Motorcycle implementation."""
    
    @property
    def wheels(self) -> int:
        return 2
    
    @property
    def vehicle_type(self) -> str:
        return "Motorcycle"


class Truck(Vehicle):
    """Truck implementation with variable wheels."""
    
    def __init__(self, brand, axles):
        super().__init__(brand)
        self._axles = axles
    
    @property
    def wheels(self) -> int:
        return self._axles * 2
    
    @property
    def vehicle_type(self) -> str:
        return f"Truck ({self._axles}-axle)"


# Abstract class with read-write property
class Account(ABC):
    """
    Abstract class with abstract setter.
    """
    
    def __init__(self, owner):
        self._owner = owner
        self._balance = 0.0
    
    @property
    def owner(self) -> str:
        return self._owner
    
    # Abstract getter and setter
    @property
    @abstractmethod
    def balance(self) -> float:
        """Get current balance."""
        pass
    
    @balance.setter
    @abstractmethod
    def balance(self, value: float):
        """Set balance - may have validation."""
        pass


class SavingsAccount(Account):
    """Savings account with minimum balance."""
    
    MINIMUM_BALANCE = 100.0
    
    @property
    def balance(self) -> float:
        return self._balance
    
    @balance.setter
    def balance(self, value: float):
        if value < self.MINIMUM_BALANCE:
            raise ValueError(f"Balance cannot be below ${self.MINIMUM_BALANCE}")
        self._balance = value


class CheckingAccount(Account):
    """Checking account with overdraft protection."""
    
    OVERDRAFT_LIMIT = -500.0
    
    @property
    def balance(self) -> float:
        return self._balance
    
    @balance.setter
    def balance(self, value: float):
        if value < self.OVERDRAFT_LIMIT:
            raise ValueError(f"Exceeded overdraft limit of ${-self.OVERDRAFT_LIMIT}")
        self._balance = value


print("=== Abstract Properties ===\n")

# Vehicle examples
print("--- Vehicles ---")
car = Car("Toyota")
motorcycle = Motorcycle("Harley")
truck = Truck("Volvo", 3)

vehicles = [car, motorcycle, truck]

for v in vehicles:
    print(f"{v.describe()}")
    print(f"  wheels property: {v.wheels}")
    print()

# Account examples
print("--- Accounts ---")

savings = SavingsAccount("Alice")
savings.balance = 500.0
print(f"Savings: {savings.owner}, balance=${savings.balance}")

# Try to go below minimum
try:
    savings.balance = 50.0
except ValueError as e:
    print(f"Error: {e}")

print()

checking = CheckingAccount("Bob")
checking.balance = 100.0
print(f"Checking: {checking.owner}, balance=${checking.balance}")

# Use overdraft
checking.balance = -300.0
print(f"After overdraft: balance=${checking.balance}")

# Try to exceed overdraft
try:
    checking.balance = -600.0
except ValueError as e:
    print(f"Error: {e}")

print("\n=== Abstract Property Rules ===")
print("""
Syntax for abstract property:
    @property
    @abstractmethod
    def my_property(self):
        pass

Syntax for abstract setter:
    @my_property.setter
    @abstractmethod
    def my_property(self, value):
        pass

Key points:
1. @property must come BEFORE @abstractmethod
2. Subclass must implement the property
3. Can have abstract getter only, or both getter and setter
4. Concrete properties work normally
""")

Combine @property with @abstractmethod for required properties.

Multiple inheritance with ABC

Inherit from multiple abstract classes.

multiple_inheritance.py
# Abstract Classes with Multiple Inheritance

from abc import ABC, abstractmethod

# Multiple abstract base classes
class Printable(ABC):
    """Abstract class for objects that can be printed."""
    
    @abstractmethod
    def to_string(self) -> str:
        """Convert to printable string."""
        pass
    
    def print(self):
        """Concrete method that uses to_string()."""
        print(f"[PRINT] {self.to_string()}")


class Serializable(ABC):
    """Abstract class for objects that can be serialized."""
    
    @abstractmethod
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        pass
    
    def to_json_string(self) -> str:
        """Concrete method that uses to_dict()."""
        import json
        return json.dumps(self.to_dict())


class Comparable(ABC):
    """Abstract class for objects that can be compared."""
    
    @abstractmethod
    def compare_to(self, other) -> int:
        """
        Compare to another object.
        Returns: negative if self < other, 0 if equal, positive if self > other
        """
        pass
    
    def __lt__(self, other):
        return self.compare_to(other) < 0
    
    def __le__(self, other):
        return self.compare_to(other) <= 0
    
    def __gt__(self, other):
        return self.compare_to(other) > 0
    
    def __ge__(self, other):
        return self.compare_to(other) >= 0
    
    def __eq__(self, other):
        return self.compare_to(other) == 0


# Class inheriting from multiple abstract classes
class Product(Printable, Serializable, Comparable):
    """
    Product implements all three abstract classes.
    Must implement: to_string, to_dict, compare_to
    """
    
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    # From Printable
    def to_string(self) -> str:
        return f"{self.name}: ${self.price:.2f} (qty: {self.quantity})"
    
    # From Serializable
    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "price": self.price,
            "quantity": self.quantity
        }
    
    # From Comparable
    def compare_to(self, other) -> int:
        # Compare by price
        if self.price < other.price:
            return -1
        elif self.price > other.price:
            return 1
        return 0


# Another class with multiple inheritance
class Employee(Printable, Serializable):
    """Employee implements Printable and Serializable."""
    
    def __init__(self, name, department, salary):
        self.name = name
        self.department = department
        self.salary = salary
    
    def to_string(self) -> str:
        return f"{self.name} ({self.department}) - ${self.salary:,.2f}"
    
    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "department": self.department,
            "salary": self.salary
        }


print("=== Multiple Inheritance with ABCs ===\n")

# Product examples
print("--- Products ---")
products = [
    Product("Laptop", 999.99, 10),
    Product("Mouse", 29.99, 50),
    Product("Keyboard", 79.99, 30),
]

# Using Printable interface
print("Using Printable.print():")
for p in products:
    p.print()

print()

# Using Serializable interface
print("Using Serializable.to_json_string():")
for p in products:
    print(f"  {p.to_json_string()}")

print()

# Using Comparable interface
print("Using Comparable (sorting by price):")
sorted_products = sorted(products)
for p in sorted_products:
    print(f"  {p.to_string()}")

print()

# Compare products directly
p1, p2 = products[0], products[1]
print(f"Comparing {p1.name} and {p2.name}:")
print(f"  {p1.name} < {p2.name}: {p1 < p2}")
print(f"  {p1.name} > {p2.name}: {p1 > p2}")
print(f"  {p1.name} == {p2.name}: {p1 == p2}")

# Employee examples
print("\n--- Employees ---")
employees = [
    Employee("Alice", "Engineering", 95000),
    Employee("Bob", "Marketing", 75000),
]

for emp in employees:
    emp.print()
    print(f"  JSON: {emp.to_json_string()}")

print("\n--- Method Resolution Order ---")
print(f"Product MRO: {[cls.__name__ for cls in Product.__mro__]}")
print(f"Employee MRO: {[cls.__name__ for cls in Employee.__mro__]}")

print("\n=== Multiple Inheritance Benefits ===")
print("""
1. Compose Behaviors:
   - Printable: adds print capability
   - Serializable: adds JSON serialization
   - Comparable: adds comparison operators

2. Interface-like Pattern:
   - Each ABC defines a small interface
   - Classes mix and match as needed

3. Code Reuse:
   - Concrete methods in ABCs provide default behavior
   - Only abstract methods need implementation

4. Python's MRO (Method Resolution Order):
   - Resolves diamond problem
   - C3 linearization algorithm
   - Use super() to delegate properly
""")

Class can implement multiple ABCs. Must implement all abstract methods.

Exercise: practical.py

Build a plugin system with abstract base classes