SOLID Principles in Depth: The Liskov Substitution Principle (LSP)
By Misbahul Munir5 min read939 words

SOLID Principles in Depth: The Liskov Substitution Principle (LSP)

Backend Development
solid principle
clean code

Continuing our journey through the SOLID principles of object-oriented design, we now arrive at the Liskov Substitution Principle (LSP)—a concept that’s easy to misinterpret, but incredibly powerful once you understand its real impact on software quality.

Let’s unpack what it means, why it matters, and how to avoid the subtle design traps it protects us from.


What is the Liskov Substitution Principle?

Definition (Barbara Liskov, 1987):

“Subtypes must be substitutable for their base types without altering the correctness of the program.”

In simpler terms:

If class

S
is a subclass of class
T
, then you should be able to use
S
anywhere
T
is expected without breaking the behavior.

It ensures that subclasses don’t just “fit” syntactically, but also preserve the expectations of the base class.


Why LSP Matters

  • Ensures reliability when using inheritance.
  • Supports polymorphism without introducing bugs.
  • Keeps code intuitive by avoiding surprise behavior in subclasses.
  • Improves reusability by allowing you to use any subclass confidently.

Classic Violation of LSP: The Rectangle & Square Problem

Let’s say we have a

Rectangle
class:

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 area(self): return self.width * self.height

Now we define a

Square
as a special kind of
Rectangle
:

class Square(Rectangle): def set_width(self, width): self.width = width self.height = width # Force height = width def set_height(self, height): self.width = height self.height = height

At first glance, this seems okay. A square is-a rectangle, right? But let’s test it:

def resize_and_print_area(rect): rect.set_width(5) rect.set_height(10) print(rect.area()) resize_and_print_area(Rectangle(2, 3)) # Output: 50 (5 * 10) resize_and_print_area(Square(2)) # Output: 100 (10 * 10) ❌

The square breaks expectations. The caller assumes width and height can be set independently. This violates LSP.


LSP-Compliant Design: Composition over Inheritance

To fix this, we can avoid inheritance and model

Square
and
Rectangle
separately:

class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height class Square: def __init__(self, size): self.size = size def area(self): return self.size * self.size

Now both types are separate abstractions with clear, predictable behavior.


Real-World Example: Payment Processing in an E-Commerce System

Imagine you're building an e-commerce microservice that integrates with multiple payment providers.

You define a base class:

class PaymentProvider: def authorize(self, amount): raise NotImplementedError def capture(self, transaction_id): raise NotImplementedError

You then implement a few providers:

class StripeProvider(PaymentProvider): def authorize(self, amount): # Call Stripe API return "stripe_txn_123" def capture(self, transaction_id): # Capture payment using Stripe return True

Then you add PayPal:

class PayPalProvider(PaymentProvider): def authorize(self, amount): # PayPal doesn't support separate authorize and capture raise NotImplementedError("PayPal uses direct charge") def capture(self, transaction_id): raise NotImplementedError("Not supported")

LSP Violation

You now cannot safely substitute

PayPalProvider
where a
PaymentProvider
is expected:

def checkout(provider: PaymentProvider): txn_id = provider.authorize(100) # This fails for PayPal provider.capture(txn_id)

Code that expects a

PaymentProvider
can break depending on the subclass—this is a clear LSP violation. The base class promises something the subclass cannot deliver.


LSP-Compliant Design: Split Interfaces Based on Behavior

The issue is that your abstraction was too broad. Fix it by splitting responsibilities:

class PaymentAuthorizer: def authorize(self, amount): raise NotImplementedError class PaymentCapturer: def capture(self, transaction_id): raise NotImplementedError

Stripe supports both:

class StripePayment(PaymentAuthorizer, PaymentCapturer): def authorize(self, amount): return "stripe_txn_123" def capture(self, transaction_id): return True

PayPal supports direct charge only:

class PayPalDirectCharge: def charge(self, amount): # PayPal's one-step charge return "paypal_txn_456"

Then your application logic becomes cleaner and safer:

def checkout_two_step(authorizer: PaymentAuthorizer, capturer: PaymentCapturer): txn_id = authorizer.authorize(100) capturer.capture(txn_id) def checkout_one_step(direct_charge_provider): txn_id = direct_charge_provider.charge(100)

Summary of the Real-World LSP Lesson

Incorrect Design (LSP Violation)Correct Design (LSP Compliant)
Subclass doesn’t implement full behaviorSubclass only implements supported behavior
Code breaks when a subclass is swappedAny subclass can be used safely and predictably
One interface forces all subclasses to conformMultiple focused interfaces separate responsibilities

Where This Happens Often

  • Cloud provider abstractions (e.g., AWS S3 vs Google Cloud Storage APIs)
  • Database drivers (some support transactions, some don’t)
  • Shipping providers (some offer tracking updates, others don’t)
  • Social login providers (different user data returned from Google, Apple, Facebook)

LSP Guidelines in Practice

To follow LSP:

  • Subclasses should honor contracts of the base class.
  • Don’t override methods in a way that changes expected behavior.
  • Avoid “dummy” implementations or raising exceptions from overridden methods.
  • Think in terms of behavioral substitutability, not just structure.

Common Signs You're Violating LSP

  • You override methods just to raise exceptions
  • A subclass “disables” some features of the superclass
  • You rely on type checks (
    if isinstance
    ) inside polymorphic code
  • Swapping a subclass causes test failures

Pro Tips

  • Always write tests that pass both the parent and child into the same function.
  • Consider using interfaces or abstract classes to define clear expectations.
  • Rethink your inheritance model when “is-a” doesn’t fully apply.

Conclusion

The Liskov Substitution Principle reminds us that inheritance is more than just code reuse—it’s about preserving behavioral contracts. Subclasses should honor the expectations set by their parent classes to keep your code safe, consistent, and extendable.

By applying LSP, you build systems that are more predictable, easier to reason about, and robust to change.


Up Next: The Interface Segregation Principle—learn how to keep your interfaces lean and focused!