โ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.
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): passclass 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_heightclass 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()}") # 50square = 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.
from abc import ABC, abstractmethodclass 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): passclass 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!
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.