Liskov Substitution Principle (LSP) Explained

Updated 2026-03-11

TL;DR

The Liskov Substitution Principle (LSP) states that objects of a subclass should be replaceable with objects of their superclass without breaking the application. If your code works with a base class, it must work identically with any derived class. Violating LSP leads to fragile inheritance hierarchies and unexpected runtime errors.

Prerequisites: Understanding of inheritance and polymorphism, familiarity with base and derived classes, basic knowledge of method overriding, and understanding of IS-A relationships in object-oriented programming.

After this topic: Identify LSP violations in inheritance hierarchies, design subclasses that properly substitute for their base types without breaking contracts, refactor code to comply with LSP using composition or interface segregation, and explain why certain inheritance relationships violate substitutability.

Core Concept

What is the Liskov Substitution Principle?

The Liskov Substitution Principle (LSP) is the ‘L’ in SOLID principles. Named after Barbara Liskov, it states: “Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.”

In simpler terms: if class Dog extends class Animal, then anywhere you use an Animal, you should be able to use a Dog instead without the code breaking or behaving unexpectedly.

Why LSP Matters

LSP ensures that inheritance hierarchies are logically sound. When you violate LSP, you create situations where:

  • Code that works with base classes fails with derived classes
  • You need type-checking (instanceof or type()) to handle special cases
  • Subclasses throw unexpected exceptions or return invalid values
  • Client code becomes fragile and tightly coupled to specific implementations

The Contract Metaphor

Think of LSP as a contract. The base class establishes a contract (what methods do, what they accept, what they return). Every subclass must honor this contract. This means:

Preconditions cannot be strengthened: If the base method accepts any integer, the subclass can’t require only positive integers.

Postconditions cannot be weakened: If the base method guarantees a non-null return, the subclass can’t return null.

Invariants must be preserved: If the base class maintains certain state rules, subclasses must maintain them too.

History constraint: Subclasses shouldn’t modify state in ways the base class doesn’t allow.

LSP vs. Simple Inheritance

Just because you can use inheritance doesn’t mean you should. The classic example: a Square is mathematically a Rectangle, but in code, Square extending Rectangle often violates LSP because a square’s width and height must always be equal—a constraint rectangles don’t have.

Visual Guide

LSP Compliant vs. Non-Compliant Hierarchies

graph TD
    A[Bird] --> B[Sparrow]
    A --> C[Penguin]
    D[Rectangle] --> E[Square]
    
    style A fill:#90EE90
    style B fill:#90EE90
    style C fill:#FFB6C6
    style D fill:#90EE90
    style E fill:#FFB6C6
    
    A1["✓ LSP Compliant"] -.-> A
    A2["✗ Penguin can't fly()"] -.-> C
    D1["✓ Rectangle has width/height"] -.-> D
    D2["✗ Square breaks width/height independence"] -.-> E

Green indicates LSP-compliant relationships; red indicates violations. Penguin violates LSP if Bird has a fly() method. Square violates LSP because setting width independently from height breaks Rectangle’s contract.

Behavioral Substitution Flow

sequenceDiagram
    participant Client
    participant BaseClass
    participant SubClass
    
    Client->>BaseClass: Call method()
    BaseClass-->>Client: Expected behavior
    
    Note over Client,SubClass: LSP: Substituting subclass
    
    Client->>SubClass: Call method()
    SubClass-->>Client: Same expected behavior
    
    Note over Client,SubClass: Client code unchanged!

LSP ensures that substituting a subclass for a base class requires no changes to client code. The behavior remains consistent with the base class contract.

Examples

Example 1: LSP Violation - Rectangle and Square

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    def set_width(self, width):
        self._width = width
    
    def set_height(self, height):
        self._height = height
    
    def get_area(self):
        return self._width * self._height


class Square(Rectangle):
    """LSP VIOLATION: Square changes Rectangle's behavior"""
    def set_width(self, width):
        self._width = width
        self._height = width  # Forced to keep square property
    
    def set_height(self, height):
        self._width = height  # Forced to keep square property
        self._height = height


# Client code that works with Rectangle
def test_rectangle(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(4)
    expected_area = 5 * 4  # 20
    actual_area = rect.get_area()
    assert actual_area == expected_area, f"Expected {expected_area}, got {actual_area}"
    print(f"✓ Area is {actual_area}")


# Test with Rectangle - works fine
rect = Rectangle(0, 0)
test_rectangle(rect)  # Output: ✓ Area is 20

# Test with Square - FAILS!
square = Square(0, 0)
test_rectangle(square)  # AssertionError: Expected 20, got 16

Expected Output:

✓ Area is 20
AssertionError: Expected 20, got 16

Why it violates LSP: The test_rectangle function works correctly with Rectangle but breaks with Square. When we set width to 5, then height to 4, the Square’s overridden methods force both dimensions to 4, giving area 16 instead of 20. The client code must now check types or handle Square specially.

Try it yourself: How would you redesign this to avoid the violation? Hint: Consider making both Rectangle and Square implement a common Shape interface without inheritance between them.


Example 2: LSP Compliant - Payment Processing

from abc import ABC, abstractmethod


class PaymentProcessor(ABC):
    """Base class defining the contract for payment processing"""
    
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        """Process payment. Returns True if successful, False otherwise.
        
        Precondition: amount > 0
        Postcondition: Returns boolean, never raises exception for valid input
        """
        pass


class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        if amount <= 0:
            return False
        print(f"Processing ${amount} via Credit Card")
        # Simulate payment processing
        return True


class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        if amount <= 0:
            return False
        print(f"Processing ${amount} via PayPal")
        # Simulate payment processing
        return True


class CryptoProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        if amount <= 0:
            return False
        print(f"Processing ${amount} via Cryptocurrency")
        # Simulate payment processing
        return True


# Client code works with any PaymentProcessor
def checkout(processor: PaymentProcessor, amount: float):
    print(f"\nInitiating checkout for ${amount}")
    if processor.process_payment(amount):
        print("✓ Payment successful!")
    else:
        print("✗ Payment failed!")


# All processors are substitutable
processors = [
    CreditCardProcessor(),
    PayPalProcessor(),
    CryptoProcessor()
]

for processor in processors:
    checkout(processor, 99.99)

Expected Output:

Initiating checkout for $99.99
Processing $99.99 via Credit Card
✓ Payment successful!

Initiating checkout for $99.99
Processing $99.99 via PayPal
✓ Payment successful!

Initiating checkout for $99.99
Processing $99.99 via Cryptocurrency
✓ Payment successful!

Why it’s LSP compliant: All subclasses honor the contract:

  • They accept the same preconditions (amount > 0)
  • They return the same type (boolean)
  • They don’t throw unexpected exceptions
  • The checkout function works identically with any processor

Try it yourself: Add a GiftCardProcessor that violates LSP by throwing an exception when the amount exceeds the card balance. Then refactor it to comply with LSP.


Example 3: LSP Violation - Bird Hierarchy

class Bird:
    def fly(self):
        return "Flying in the sky"


class Sparrow(Bird):
    def fly(self):
        return "Sparrow flying fast"


class Penguin(Bird):
    """LSP VIOLATION: Penguins can't fly"""
    def fly(self):
        raise Exception("Penguins can't fly!")


# Client code expects all birds to fly
def make_bird_fly(bird: Bird):
    try:
        result = bird.fly()
        print(result)
    except Exception as e:
        print(f"Error: {e}")


birds = [Sparrow(), Penguin()]

for bird in birds:
    make_bird_fly(bird)

Expected Output:

Sparrow flying fast
Error: Penguins can't fly!

Why it violates LSP: The client code must handle exceptions for Penguin, even though it works fine with the base Bird class. This forces defensive programming and type-checking.

LSP-Compliant Solution:

from abc import ABC, abstractmethod


class Bird(ABC):
    @abstractmethod
    def move(self):
        pass


class FlyingBird(Bird):
    def move(self):
        return self.fly()
    
    def fly(self):
        return "Flying in the sky"


class Sparrow(FlyingBird):
    def fly(self):
        return "Sparrow flying fast"


class Penguin(Bird):
    def move(self):
        return "Penguin swimming"


# Client code works with any Bird
def make_bird_move(bird: Bird):
    print(bird.move())


birds = [Sparrow(), Penguin()]

for bird in birds:
    make_bird_move(bird)  # Works for all birds!

Expected Output:

Sparrow flying fast
Penguin swimming

Try it yourself: Add an Ostrich class that runs instead of flying. Ensure it fits into the hierarchy without violating LSP.


Java/C++ Notes

Java:

// Use interfaces to define contracts
public interface PaymentProcessor {
    boolean processPayment(double amount);
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        if (amount <= 0) return false;
        System.out.println("Processing $" + amount + " via Credit Card");
        return true;
    }
}

C++:

// Use pure virtual functions for contracts
class PaymentProcessor {
public:
    virtual bool processPayment(double amount) = 0;
    virtual ~PaymentProcessor() = default;
};

class CreditCardProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        if (amount <= 0) return false;
        std::cout << "Processing $" << amount << " via Credit Card\n";
        return true;
    }
};

Common Mistakes

1. Strengthening Preconditions in Subclasses

Mistake: A subclass requires stricter input than the base class.

class FileReader:
    def read(self, filename: str) -> str:
        """Reads any file"""
        with open(filename) as f:
            return f.read()

class TextFileReader(FileReader):
    def read(self, filename: str) -> str:
        """VIOLATION: Only accepts .txt files"""
        if not filename.endswith('.txt'):
            raise ValueError("Only .txt files allowed")
        return super().read(filename)

Why it’s wrong: Code expecting FileReader to handle any file will break with TextFileReader. The subclass strengthened the precondition (must be .txt), violating LSP.

Fix: Either make the base class also require .txt files, or don’t restrict the subclass.


2. Weakening Postconditions in Subclasses

Mistake: A subclass returns less than what the base class promises.

class UserRepository:
    def get_user(self, user_id: int) -> dict:
        """Returns user dict with 'id', 'name', 'email'"""
        return {'id': user_id, 'name': 'John', 'email': 'john@example.com'}

class CachedUserRepository(UserRepository):
    def get_user(self, user_id: int) -> dict:
        """VIOLATION: Might return None if not cached"""
        cached = self._check_cache(user_id)
        return cached  # Could be None!
    
    def _check_cache(self, user_id):
        return None  # Simulating cache miss

Why it’s wrong: Client code expects a dict but might get None, causing AttributeError when accessing keys.

Fix: Always return a dict, even if empty, or fetch from the database on cache miss.


3. Throwing New Exceptions

Mistake: A subclass throws exceptions the base class doesn’t throw.

class Calculator:
    def divide(self, a: float, b: float) -> float:
        """Returns a/b, returns 0 if b is 0"""
        return a / b if b != 0 else 0

class StrictCalculator(Calculator):
    def divide(self, a: float, b: float) -> float:
        """VIOLATION: Throws exception instead of returning 0"""
        if b == 0:
            raise ZeroDivisionError("Cannot divide by zero")
        return a / b

Why it’s wrong: Code using Calculator doesn’t expect exceptions and will crash with StrictCalculator.

Fix: Match the base class behavior or document the exception in the base class contract.


4. Changing Behavior Semantically

Mistake: A subclass does something fundamentally different than expected.

class Stack:
    def __init__(self):
        self._items = []
    
    def push(self, item):
        """Adds item to top of stack"""
        self._items.append(item)
    
    def pop(self):
        """Removes and returns top item"""
        return self._items.pop()

class RandomStack(Stack):
    def pop(self):
        """VIOLATION: Returns random item instead of top"""
        import random
        if not self._items:
            raise IndexError("pop from empty stack")
        idx = random.randint(0, len(self._items) - 1)
        return self._items.pop(idx)

Why it’s wrong: A stack has LIFO semantics. Returning a random item violates the fundamental contract, even if the method signature is the same.

Fix: Don’t call it a Stack. Create a separate RandomCollection class.


5. Refusing to Implement Inherited Methods

Mistake: A subclass makes inherited methods unusable.

class List:
    def add(self, item):
        pass
    
    def remove(self, item):
        pass

class ImmutableList(List):
    def add(self, item):
        raise NotImplementedError("Cannot modify immutable list")
    
    def remove(self, item):
        raise NotImplementedError("Cannot modify immutable list")

Why it’s wrong: Code expecting a List will fail with ImmutableList. The subclass refuses to honor the base class contract.

Fix: Use composition instead of inheritance. Create an ImmutableList that doesn’t inherit from List, or use an interface that only defines read operations.

Interview Tips

1. Recognize LSP Violations in Code Reviews

Interviewers often present code and ask, “What’s wrong here?” Practice identifying these red flags:

  • Subclass throws exceptions the base class doesn’t
  • Subclass returns null when base class guarantees non-null
  • Subclass has empty or no-op implementations of base methods
  • Subclass requires type-checking in client code (if isinstance(obj, SubClass))

Interview Question: “Is this code LSP-compliant? Why or why not?”

Your Answer Framework:

  1. Identify the base class contract (what it promises)
  2. Check if the subclass honors all promises
  3. Consider if client code would break when substituting
  4. Suggest a fix if violated

2. Explain the Rectangle-Square Problem

This is the most common LSP interview question. Be ready to:

  • Explain why Square inheriting from Rectangle violates LSP
  • Discuss the difference between mathematical IS-A and behavioral IS-A
  • Propose solutions (separate classes, interfaces, immutable objects)

Sample Answer: “While mathematically a square is a rectangle, in code, Square violates LSP if it inherits from Rectangle. Setting width and height independently is part of Rectangle’s contract. Square must keep them equal, breaking that contract. Client code expecting independent dimensions will fail. Better solution: both implement a Shape interface, or use immutable objects where dimensions are set only at construction.”


3. Connect LSP to Real-World Design Decisions

Interviewers want to see you apply LSP to practical scenarios:

  • “How would you design a payment processing system?” → Show substitutable processors
  • “Design a notification system” → Show substitutable notifiers (email, SMS, push)
  • “Implement a caching layer” → Show cache implementations that don’t break the repository contract

Key Point: Always mention that LSP helps you avoid type-checking and makes code extensible.


4. Discuss Trade-offs

Senior interviews expect nuance. Mention:

  • When to use composition over inheritance: If you’re forcing LSP compliance, composition might be cleaner
  • Interface Segregation: Sometimes LSP violations indicate the base class is too broad
  • Performance vs. Purity: In rare cases, slight LSP violations might be acceptable for performance (document them!)

Example: “If strict LSP compliance requires significant refactoring and the violation is isolated and well-documented, I might accept it temporarily. But I’d add a TODO and plan to refactor, because LSP violations tend to spread and cause bugs.”


5. Code the Fix, Not Just Identify the Problem

Interviewers often follow up with “How would you fix this?” Have these patterns ready:

Pattern 1: Extract Interface

# Instead of inheritance, use interface
class Shape(ABC):
    @abstractmethod
    def area(self): pass

class Rectangle(Shape): ...
class Square(Shape): ...

Pattern 2: Composition

# Instead of inheriting, contain
class ReadOnlyList:
    def __init__(self, items):
        self._list = list(items)  # Composition
    
    def get(self, index):
        return self._list[index]

Pattern 3: Split the Hierarchy

# Create more specific base classes
class Bird(ABC): pass
class FlyingBird(Bird): ...
class FlightlessBird(Bird): ...

6. Use Precise Terminology

Impress interviewers with correct terms:

  • Preconditions (what must be true before method call)
  • Postconditions (what must be true after method call)
  • Invariants (what must always be true)
  • Behavioral subtyping (LSP’s formal name)
  • Contract (the promises a class makes)

Example: “The subclass strengthens the precondition by requiring positive integers when the base class accepts any integer, violating behavioral subtyping.”


7. Relate to Other SOLID Principles

Show you understand how principles interact:

  • LSP + Open/Closed: LSP enables OCP by making subclasses safely extensible
  • LSP + Interface Segregation: ISP can help fix LSP violations by creating smaller, more focused contracts
  • LSP + Dependency Inversion: Both rely on abstractions and contracts

Interview Gold: “LSP violations often indicate we’re violating other SOLID principles too. For example, if a subclass refuses to implement a method, we might need Interface Segregation to split the interface.”

Key Takeaways

  • LSP ensures subclasses are true substitutes: Any code working with a base class must work identically with any subclass without modification or type-checking.

  • Honor the contract: Subclasses cannot strengthen preconditions, weaken postconditions, or throw new exceptions not declared by the base class.

  • Behavioral IS-A vs. mathematical IS-A: Just because something “is a” something else conceptually doesn’t mean inheritance is correct in code (Square/Rectangle problem).

  • Red flags for LSP violations: Empty method implementations, type-checking in client code, unexpected exceptions, and methods that refuse to work are all signs of LSP violations.

  • Fix violations with composition or interfaces: When inheritance doesn’t work, use composition, extract interfaces, or split the hierarchy into more specific base classes.