Skip to main content

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.

๐Ÿ”„ 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!
The Test: If your code works with a parent class, it should work with ANY child class without knowing the difference!
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:
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

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

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

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:
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:
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:
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:
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

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

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

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

This code violates LSP. Fix it!
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!

๐Ÿ“ Quick LSP Checklist

CheckQuestion
โœ… 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

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.

๐Ÿƒ Next: Interface Segregation Principle

Letโ€™s learn why smaller, focused interfaces are better than big ones!

Continue to Interface Segregation โ†’

Learn why many small interfaces beat one big interface!