> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Liskov Substitution Principle

> Children should behave like parents - no surprises!

## 🔄 The LSP Rule

> **"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."**

In simple terms: If you expect a **Bird**, any type of bird (Sparrow, Eagle, Parrot) should work the same way!

<Tip>
  **The Test**: If your code works with a parent class, it should work with ANY child class without knowing the difference!
</Tip>

**The rental car analogy:** When you rent a car, you expect it to have a steering wheel, pedals, and an ignition. Whether you get a Toyota Camry or a BMW 3 Series, the basic driving interface works the same. You would be shocked if you got a vehicle where pressing the brake made it accelerate. That is LSP: any subclass must honor the behavioral contract of its parent. The caller should never need to check "wait, which specific type did I get?" before using it safely.

***

## 🐧 The Famous Penguin Problem

The classic LSP violation:

```python theme={null}
class Bird:
    def fly(self):
        print("Flying high! 🦅")

class Sparrow(Bird):
    def fly(self):
        print("Sparrow flying! 🐦")

class Penguin(Bird):  # 🚨 PROBLEM!
    def fly(self):
        raise Exception("I can't fly! 🐧")  # Surprise!

# Code that works with Bird:
def make_bird_fly(bird: Bird):
    bird.fly()  # Expects all birds to fly

# Works fine:
make_bird_fly(Sparrow())  # ✅ Works!

# CRASHES:
make_bird_fly(Penguin())  # 💥 Exception! LSP violated!
```

**The Problem**: The code expects ALL birds to fly, but Penguin breaks that expectation!

***

## ✅ The Correct Solution

```python theme={null}
from abc import ABC, abstractmethod

class Bird(ABC):
    """Base bird - only common behavior"""
    @abstractmethod
    def move(self):
        pass
    
    @abstractmethod
    def eat(self):
        pass

class FlyingBird(Bird):
    """Birds that can fly"""
    def move(self):
        self.fly()
    
    def fly(self):
        print("Flying through the air! 🦅")
    
    def eat(self):
        print("Pecking at food 🌾")

class SwimmingBird(Bird):
    """Birds that swim"""
    def move(self):
        self.swim()
    
    def swim(self):
        print("Swimming in water! 🏊")
    
    def eat(self):
        print("Catching fish 🐟")

class Sparrow(FlyingBird):
    def fly(self):
        print("Sparrow flying! 🐦")

class Eagle(FlyingBird):
    def fly(self):
        print("Eagle soaring! 🦅")

class Penguin(SwimmingBird):
    def swim(self):
        print("Penguin diving deep! 🐧")

class Duck(FlyingBird, SwimmingBird):  # Can do both!
    def fly(self):
        print("Duck flying! 🦆")
    
    def swim(self):
        print("Duck paddling! 🦆")
    
    def move(self):
        self.fly()  # Prefers flying

# Now any Bird can move() - no surprises!
def make_bird_move(bird: Bird):
    bird.move()  # Works for ALL birds!

make_bird_move(Sparrow())   # ✅ Flying!
make_bird_move(Penguin())   # ✅ Swimming!
make_bird_move(Eagle())     # ✅ Flying!
```

***

## 📏 The Rectangle-Square Problem

Another famous LSP violation:

### ❌ BAD: Square extends Rectangle

```python theme={null}
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    @property
    def width(self):
        return self._width
    
    @width.setter
    def width(self, value):
        self._width = value
    
    @property
    def height(self):
        return self._height
    
    @height.setter
    def height(self, value):
        self._height = value
    
    def area(self):
        return self._width * self._height

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)
    
    @Rectangle.width.setter
    def width(self, value):
        # 🚨 Must keep width = height for square!
        self._width = value
        self._height = value  # Surprise side effect!
    
    @Rectangle.height.setter
    def height(self, value):
        # 🚨 Must keep width = height for square!
        self._width = value  # Surprise side effect!
        self._height = value

# Code that works with Rectangle:
def resize_rectangle(rect: Rectangle, new_width, new_height):
    rect.width = new_width
    rect.height = new_height
    # Expect area = width * height
    expected_area = new_width * new_height
    actual_area = rect.area()
    print(f"Expected: {expected_area}, Actual: {actual_area}")
    assert actual_area == expected_area  # 💥 Fails for Square!

# Test:
rect = Rectangle(10, 20)
resize_rectangle(rect, 5, 10)  # ✅ Works! Expected: 50, Actual: 50

square = Square(10)
resize_rectangle(square, 5, 10)  # 💥 Fails! Expected: 50, Actual: 100
# Because setting height=10 ALSO set width=10!
```

### ✅ GOOD: Separate Shapes

```python theme={null}
from abc import ABC, abstractmethod

# DESIGN REASONING: Rather than forcing Square to be a child of
# Rectangle (which breaks the resize contract), we make both
# siblings under a common Shape parent. Each defines its own
# resize semantics that match its geometry. No surprises.

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def resize(self, new_width, new_height):
        self.width = new_width
        self.height = new_height

class Square(Shape):
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2
    
    def resize(self, new_side):
        self.side = new_side

# Now each shape behaves correctly!
rect = Rectangle(10, 20)
rect.resize(5, 10)
print(f"Rectangle area: {rect.area()}")  # 50

square = Square(10)
square.resize(5)
print(f"Square area: {square.area()}")  # 25
```

**Key insight:** "Is-a" in mathematics does not always mean "is-a" in software. A square *is* a rectangle mathematically, but in code, the behavioral contract (independently mutable width and height) breaks for squares. LSP teaches you to think about behavioral compatibility, not just conceptual taxonomy.

***

## 🎯 LSP Rules to Follow

### Rule 1: Method Signatures

Child methods should accept the **same or broader** input types:

```python theme={null}
class Parent:
    def process(self, data: list):
        pass

class GoodChild(Parent):
    def process(self, data: list):  # ✅ Same type
        pass

class BadChild(Parent):
    def process(self, data: list[int]):  # ❌ More restrictive!
        pass
```

### Rule 2: Return Types

Child methods should return the **same or narrower** types:

```python theme={null}
class Parent:
    def get_value(self) -> object:
        return "anything"

class GoodChild(Parent):
    def get_value(self) -> str:  # ✅ More specific (str is object)
        return "string only"

class BadChild(Parent):
    def get_value(self) -> str | None:  # ❌ Might return None!
        return None
```

### Rule 3: No New Exceptions

Children shouldn't throw exceptions parents didn't:

```python theme={null}
class Parent:
    def save(self, data):
        # Might raise IOError
        pass

class GoodChild(Parent):
    def save(self, data):
        # Also might raise IOError - same as parent
        pass

class BadChild(Parent):
    def save(self, data):
        raise PermissionError("Surprise!")  # ❌ New exception type!
```

### Rule 4: Honor Invariants

If parent guarantees something, child must too:

```python theme={null}
class BankAccount:
    """Invariant: balance >= 0"""
    def __init__(self, balance):
        self._balance = max(0, balance)
    
    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount

class BadOverdraftAccount(BankAccount):
    def withdraw(self, amount):
        self._balance -= amount  # ❌ Breaks invariant! Balance can be negative!

class GoodOverdraftAccount(BankAccount):
    def __init__(self, balance, overdraft_limit=100):
        super().__init__(balance)
        self.overdraft_limit = overdraft_limit
    
    def withdraw(self, amount):
        if amount > self._balance + self.overdraft_limit:
            raise ValueError("Exceeds overdraft limit")
        self._balance -= amount  # ✅ New invariant: balance >= -overdraft_limit
```

***

## 🎮 Real Example: File System

### ❌ BAD: ReadOnlyFile breaks File contract

```python theme={null}
class File:
    def read(self):
        pass
    
    def write(self, content):
        pass

class RegularFile(File):
    def __init__(self, path):
        self.path = path
        self.content = ""
    
    def read(self):
        return self.content
    
    def write(self, content):
        self.content = content

class ReadOnlyFile(File):  # 🚨 Problem!
    def __init__(self, path):
        self.path = path
        self.content = "Read only content"
    
    def read(self):
        return self.content
    
    def write(self, content):
        raise PermissionError("Cannot write to read-only file!")  # 💥

# Code expecting File:
def save_to_file(file: File, data):
    file.write(data)  # Works for File...
    print("Saved!")

save_to_file(RegularFile("test.txt"), "Hello")  # ✅ Works
save_to_file(ReadOnlyFile("config.txt"), "Hello")  # 💥 Crashes!
```

### ✅ GOOD: Separate Interfaces

```python theme={null}
from abc import ABC, abstractmethod

class Readable(ABC):
    @abstractmethod
    def read(self) -> str:
        pass

class Writable(ABC):
    @abstractmethod
    def write(self, content: str):
        pass

class ReadWriteFile(Readable, Writable):
    def __init__(self, path):
        self.path = path
        self.content = ""
    
    def read(self):
        return self.content
    
    def write(self, content):
        self.content = content

class ReadOnlyFile(Readable):  # Only implements Readable
    def __init__(self, path, content):
        self.path = path
        self.content = content
    
    def read(self):
        return self.content

# Now functions specify what they need:
def read_data(source: Readable) -> str:
    return source.read()  # Works for anything Readable

def save_data(target: Writable, data: str):
    target.write(data)  # Only accepts Writable!

# Usage:
rw_file = ReadWriteFile("data.txt")
ro_file = ReadOnlyFile("config.txt", "settings...")

print(read_data(rw_file))  # ✅
print(read_data(ro_file))  # ✅

save_data(rw_file, "new data")  # ✅
# save_data(ro_file, "new data")  # ❌ Type error! ReadOnlyFile isn't Writable
```

***

## 🐾 Real Example: Pet Store

```python theme={null}
from abc import ABC, abstractmethod

class Pet(ABC):
    """Contract: All pets can be fed and can play"""
    
    @abstractmethod
    def feed(self, food: str):
        pass
    
    @abstractmethod
    def play(self):
        pass
    
    @abstractmethod
    def make_sound(self):
        pass

class Dog(Pet):
    def feed(self, food):
        print(f"🐕 Dog eating {food}")
    
    def play(self):
        print("🐕 Dog playing fetch!")
    
    def make_sound(self):
        print("🐕 Woof woof!")

class Cat(Pet):
    def feed(self, food):
        print(f"🐱 Cat eating {food}")
    
    def play(self):
        print("🐱 Cat chasing laser!")
    
    def make_sound(self):
        print("🐱 Meow!")

class Parrot(Pet):
    def feed(self, food):
        print(f"🦜 Parrot eating {food}")
    
    def play(self):
        print("🦜 Parrot solving puzzle!")
    
    def make_sound(self):
        print("🦜 Hello! Pretty bird!")

# Pet store can handle ANY pet the same way!
class PetStore:
    def __init__(self):
        self.pets = []
    
    def add_pet(self, pet: Pet):
        self.pets.append(pet)
    
    def morning_routine(self):
        print("🌅 Morning at the pet store!\n")
        for pet in self.pets:
            pet.feed("breakfast")
            pet.make_sound()
            print()
    
    def playtime(self):
        print("🎮 Playtime!\n")
        for pet in self.pets:
            pet.play()
            print()

# Any Pet subclass works perfectly!
store = PetStore()
store.add_pet(Dog())
store.add_pet(Cat())
store.add_pet(Parrot())

store.morning_routine()
store.playtime()

# 🎉 Adding new pets is easy and safe!
class Fish(Pet):
    def feed(self, food):
        print(f"🐠 Fish eating {food}")
    
    def play(self):
        print("🐠 Fish swimming around!")
    
    def make_sound(self):
        print("🐠 Blub blub...")

store.add_pet(Fish())
store.playtime()  # ✅ Works perfectly!
```

***

## 🧪 Practice Exercise

<Accordion title="Challenge: Fix the Employee Hierarchy" icon="dumbbell">
  This code violates LSP. Fix it!

  ```python theme={null}
  class Employee:
      def __init__(self, name, salary):
          self.name = name
          self.salary = salary
      
      def calculate_bonus(self):
          return self.salary * 0.1  # 10% bonus
      
      def get_annual_salary(self):
          return self.salary * 12 + self.calculate_bonus()

  class FullTimeEmployee(Employee):
      def calculate_bonus(self):
          return self.salary * 0.15  # 15% bonus ✅ OK

  class Intern(Employee):  # 🚨 Problem!
      def calculate_bonus(self):
          raise NotImplementedError("Interns don't get bonuses!")
      
      def get_annual_salary(self):
          # Only 6 months internship
          return self.salary * 6  # No bonus call here

  # This code expects Employee:
  def print_employee_report(employee: Employee):
      print(f"Employee: {employee.name}")
      print(f"Annual Salary: ${employee.get_annual_salary()}")
      print(f"Bonus: ${employee.calculate_bonus()}")  # 💥 Crashes for Intern!

  print_employee_report(FullTimeEmployee("Alice", 5000))  # ✅
  print_employee_report(Intern("Bob", 2000))  # 💥 NotImplementedError!
  ```

  <details>
    <summary>Click for Solution</summary>

    ```python theme={null}
    from abc import ABC, abstractmethod

    class Employee(ABC):
        def __init__(self, name, salary):
            self.name = name
            self.salary = salary
        
        @abstractmethod
        def get_annual_compensation(self):
            """Total annual pay including any bonuses"""
            pass

    class BonusEligibleEmployee(Employee):
        """Employees who receive bonuses"""
        
        @abstractmethod
        def calculate_bonus(self):
            pass
        
        def get_annual_compensation(self):
            return self.salary * 12 + self.calculate_bonus()

    class FullTimeEmployee(BonusEligibleEmployee):
        def calculate_bonus(self):
            return self.salary * 0.15  # 15% bonus

    class Manager(BonusEligibleEmployee):
        def calculate_bonus(self):
            return self.salary * 0.25  # 25% bonus

    class NonBonusEmployee(Employee):
        """Employees without bonus eligibility"""
        
        @abstractmethod
        def get_contract_months(self):
            pass
        
        def get_annual_compensation(self):
            return self.salary * self.get_contract_months()

    class Intern(NonBonusEmployee):
        def get_contract_months(self):
            return 6  # 6-month internship

    class Contractor(NonBonusEmployee):
        def __init__(self, name, salary, months):
            super().__init__(name, salary)
            self._months = months
        
        def get_contract_months(self):
            return self._months

    # Now report works with ANY Employee!
    def print_employee_report(employee: Employee):
        print(f"\n{'='*40}")
        print(f"Employee: {employee.name}")
        print(f"Annual Compensation: ${employee.get_annual_compensation():.2f}")
        
        # Only show bonus for bonus-eligible employees
        if isinstance(employee, BonusEligibleEmployee):
            print(f"Includes Bonus: ${employee.calculate_bonus():.2f}")
        print(f"{'='*40}")

    # All work correctly!
    print_employee_report(FullTimeEmployee("Alice", 5000))  # ✅
    print_employee_report(Manager("Charlie", 8000))  # ✅
    print_employee_report(Intern("Bob", 2000))  # ✅ No crash!
    print_employee_report(Contractor("Dave", 6000, 8))  # ✅
    ```
  </details>
</Accordion>

***

## 📝 Quick LSP Checklist

| Check            | Question                                |
| ---------------- | --------------------------------------- |
| ✅ Same behavior? | Does child method behave like parent's? |
| ✅ No surprises?  | Does child throw unexpected exceptions? |
| ✅ Replaceable?   | Can you swap parent for child anywhere? |
| ✅ Same contract? | Does child honor parent's promises?     |

***

## Interview Insight

<Info>
  **LSP is the hardest SOLID principle to demonstrate in interviews**, but it creates the strongest impression when you do. The typical way it surfaces: you design a class hierarchy, and the interviewer probes an edge case. "What about a read-only user -- can they inherit from User?" If the User parent class has a `save()` method and ReadOnlyUser throws an exception on save, that is an LSP violation. The strong answer: "I would not make ReadOnlyUser extend User because it cannot honor the save() contract. Instead, I would create a Viewable interface that both share, and only give the writable path to full User objects." The moment you use the phrase "honor the behavioral contract of the parent," you signal deep understanding. The practical test: anywhere your code does `isinstance` checks to handle a specific subclass differently, that is a sign LSP might be violated -- you are compensating for a subclass that does not truly substitute.
</Info>

***

## 🏃 Next: Interface Segregation Principle

Let's learn why smaller, focused interfaces are better than big ones!

<Card title="Continue to Interface Segregation →" icon="arrow-right" href="/lld/solid/isp">
  Learn why many small interfaces beat one big interface!
</Card>
