What Is the Liskov Substitution Principle?

What Is the Liskov Substitution Principle?

Imagine swapping one toy battery for another and suddenly your flashlight starts playing music instead of lighting up. That’s the kind of surprise good software tries to avoid — and the Liskov Substitution Principle is the rule that keeps it from happening.

01

The Big Idea, in One Breath

Think about AA batteries. It doesn’t matter which brand you buy — every single one fits into the same slot, pushes power the same direction, and makes your torch, remote control, or toy car work exactly the way it’s supposed to. You never have to open the battery compartment and check the label before you trust it to do its job. That trust is the whole point.

Now imagine a company invented a “battery” that fit the same slot perfectly but, instead of producing electricity, played a tiny tune. It looks like a battery. It clicks into place like a battery. But the moment you rely on it to power your torch, everything breaks. It technically fits — yet it can’t be trusted to behave like the thing it’s pretending to be.

That is precisely the problem the Liskov Substitution Principle — usually shortened to LSP — was created to prevent, except instead of batteries and torches, it’s talking about classes and objects in a computer program. LSP is the third principle in the famous SOLID set of object-oriented design principles, and it’s named after Barbara Liskov, a computer scientist who first described the idea in a keynote speech in 1987. Her core message, boiled down to one sentence, is this: if one type of object claims to be a more specific version of another, it must be safe to swap the two without anything going wrong.

Everyday Analogy

Picture a cookie-cutter that’s shaped like a duck. Every shape you cut with it should come out looking and behaving like a duck-shaped cookie — same outline, same size, nothing surprising. If someone swapped in a “duck” cutter that secretly produced star-shaped cookies instead, your whole batch of party favours would be ruined, even though the box still said “duck cutter” on the label. Software objects need the same kind of honesty: if it’s labelled as a certain type, it must behave like that type, every single time.

02

What LSP Really Says

Barbara Liskov’s original wording was fairly technical and mathematical, written for an audience of researchers. Over the years, engineers have translated it into a version that’s much easier to hold onto: objects of a parent (or “base”) type should be replaceable with objects of any of its child (or “sub”) types, without changing whether the program behaves correctly.

In plainer words still: if your code was written to work with a “Shape,” it should keep working — without any special-case checks — no matter whether you hand it a Circle, a Square, or a Triangle, as long as all three are declared as Shapes. The moment your code needs to ask “wait, is this actually a Circle or a Square before I can trust it?”, something about the design has gone wrong.

Client Code expects a “Shape” Shape base type Circle ✓ safe swap Square ✓ safe swap
Client code only ever talks to “Shape.” Whichever subtype actually shows up underneath should never cause a surprise.
i
In Plain Words

If someone asks “does this class follow LSP?”, they’re really asking: “Could I hand any of its subclasses to my existing code and have it keep working correctly, without me needing to add a single extra check?”

Two smaller ideas sit underneath that big one. First, substitutability is about behaviour, not just structure — a subclass can have all the right method names and the right inputs and outputs on paper, and still violate LSP if it behaves differently underneath. Second, a subtype should only ever specialise, never contradict, the promises made by its parent. It’s allowed to do more; it’s never allowed to do less, or to do something different while pretending it’s the same thing.

03

A Little History: Where the Idea Came From

Barbara Liskov didn’t invent this idea in isolation — she was one of the pioneers of a whole way of thinking about software called abstract data types, which is really just a fancy name for “types that hide how they work internally and only promise what they do.” Long before most programming languages even had classes and inheritance, Liskov was already asking a deceptively simple question: if I build a new type using an old one, how do I know the new type still keeps every promise the old one made?

She first raised this question publicly in a keynote speech in 1987, at a conference about object-oriented programming. It wasn’t originally called “the Liskov Substitution Principle” at all — that catchy name came later, once other engineers started referencing her idea so often that it needed a memorable label. In 1994, Liskov teamed up with fellow computer scientist Jeannette Wing to write it up formally, describing precisely what it means for one type to be a trustworthy “behavioural subtype” of another.

Around the same time, a related idea was taking shape under a different name: Design by Contract, popularised by computer scientist Bertrand Meyer through the Eiffel programming language. Design by Contract treats every method as if it were a legal agreement — spelling out preconditions, postconditions, and invariants explicitly, sometimes even checking them automatically while the program runs. LSP and Design by Contract grew up side by side, reinforcing the same underlying truth: good software isn’t just about what a method does when everything goes right — it’s about the promises it makes, and never quietly breaking them.

i
Good to Know

Robert C. Martin, the engineer who bundled five separate ideas together into the acronym “SOLID” in the early 2000s, chose Liskov’s idea as the “L” specifically because it captured, in one crisp rule, a mistake he’d seen experienced developers make again and again with inheritance.

04

Why It Matters So Much

You might wonder — so what if a subclass behaves a little differently? In small projects, it might genuinely not matter much. But the moment a codebase grows, gets touched by more than one engineer, and starts depending on inheritance to add new features, LSP becomes one of the quiet forces holding the whole thing together. Here’s what following it actually buys a team:

Trust

Code you don’t have to double-check

Engineers can write logic against a base type once, and trust every future subtype to slot in correctly — no “just in case” checks needed.

Fewer Bugs

No hidden landmines

Bugs caused by silent behavioural surprises are some of the hardest to trace, because the code “looks” correct at a glance.

Extensibility

New features without fear

New subclasses can be added later without reopening and rewriting code that already works — a core promise of good architecture.

Testability

Write the test once

A single suite of tests written against the base type can be reused to validate every subtype, saving huge amounts of duplicated test-writing effort.

There’s also a very practical, everyday reason this matters: teams almost never work with just one version of a “Shape,” a “Payment Method,” or a “User Account.” New subtypes get added constantly as a product grows — new payment providers, new device types, new user roles. If the very first version of the base type was designed carelessly, every single new subtype added later inherits that same shaky foundation, and the problem compounds year after year instead of fading away.

Picture a small online store that started with a single Payment class, used only for credit cards. Eighteen months later, the same class has been quietly stretched to also represent gift cards, wallet balances, and buy-now-pay-later plans — none of which can honestly support every promise the original class made, like “refund() always returns the money to the original source instantly.” Engineers start adding “if this payment type is a gift card, skip this step” checks throughout the checkout flow, and within a year, nobody on the team fully trusts the Payment class anymore. That slow erosion of trust, multiplied across dozens of classes in a large system, is what LSP quietly exists to prevent.

A subtype’s job is to keep promises, not rewrite them.
05

The Hidden Contract Every Type Signs

Here’s a helpful way to picture it. Imagine every class in your program has quietly signed an invisible contract with everyone who will ever use it. That contract has three parts, and understanding them is the real key to understanding LSP.

  1. Preconditions — what must be true before a piece of code runs. (“You must hand me a positive number.”)
  2. Postconditions — what is guaranteed to be true after the code finishes. (“I promise to return a sorted list.”)
  3. Invariants — things that must always stay true, no matter what happens in between. (“The bank balance can never go below zero.”)
PRECONDITION What must be true before we start “amount > 0” POSTCONDITION What is promised once we finish “returns receipt” INVARIANT What never changes, ever “balance ≥ 0”
Every method quietly makes these three kinds of promises. A subclass must never break any of them.

LSP adds a simple, strict rule on top of this contract: a subclass is allowed to loosen preconditions (ask for less than the parent did) but never tighten them (ask for more). It’s allowed to strengthen postconditions (promise more than the parent did) but never weaken them (promise less). And it must never break an invariant the parent guaranteed, under any circumstance.

Everyday Analogy

Think of it like a food delivery promise. If a restaurant promises “delivery within 30 minutes,” a new branch of that restaurant is allowed to promise “delivery within 20 minutes” — that’s a nice surprise, a stronger promise. But it is not allowed to quietly change the promise to “delivery within 45 minutes” while still using the same “30-minute delivery” badge on the app. Customers trusted that badge, and breaking the promise behind it — even a little — breaks their trust in every restaurant carrying that badge.

In real codebases, these contracts are rarely written down as formally as a legal document — they usually live as comments, as documentation, or simply as the shared understanding a team builds over time. That’s exactly why tests matter so much here. A well-written test doesn’t just check “does this method return the right answer for one example” — a good LSP-aware test checks “does this method still honour every precondition, postcondition, and invariant the base type promised, even for a subtype nobody has written yet.” Teams that write their tests against the base type’s contract, rather than against one specific subclass, end up with a safety net that automatically protects every future subtype too.

06

Two Kinds of Rules: Signature and Behaviour

When people study LSP closely, they usually find it helpful to split the rules into two families. One family can often be checked automatically by the programming language itself. The other family cannot — no compiler in the world can catch it, which is exactly why it causes so many hidden bugs.

Rule TypeWhat It ChecksWho Enforces It
Signature RulesMethod names, parameter types, return types, and which errors a method is allowed to raiseUsually the compiler or type-checker
Behavioural RulesPreconditions, postconditions, invariants, and whether the outcome actually matches what was promisedOnly careful design, code review, and tests

Signature rules are the easy half. Most modern languages already stop you from, say, overriding a method and suddenly requiring a completely different type of input — the compiler simply won’t let the code build. This is why so many engineers assume that once their code compiles cleanly, LSP is automatically satisfied. It isn’t. A subclass can have a perfectly matching signature and still lie about what it actually does once it starts running — and that lie is where nearly every real LSP violation lives.

Rule of Thumb

If your compiler is happy but your test suite starts throwing strange, unexplained failures whenever a new subclass gets used, you’re very likely staring at a behavioural LSP violation — not a bug in your tests.

Inside the signature rules, there are two smaller, slightly technical ideas worth knowing, because they explain why some languages allow certain overrides and forbid others. A subclass method is allowed to accept a broader range of input types than the parent promised (this is called being contravariant on parameters — think of it as “I’ll happily accept anything you throw at me, and maybe more”). A subclass method is allowed to return a narrower, more specific type than the parent promised (called being covariant on return types — “I’ll give you back something at least as specific as promised, maybe more precise”). Getting these backwards — demanding a narrower input or returning something broader than promised — is exactly the kind of signature-level mistake that breaks substitutability, and thankfully, it’s one of the few LSP mistakes most modern compilers will actually catch for you.

07

The Classic Example: Rectangle and Square

Almost every explanation of LSP eventually reaches the same famous example, and it’s famous for a good reason — it’s a small, honest trap that catches even experienced engineers. Mathematically, a square genuinely is a special kind of rectangle, one where all four sides happen to be equal. So it feels completely natural to write a Square class that extends a Rectangle class. That’s where the trouble quietly begins.

python — the tempting but risky design
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def set_width(self, value):
        self.width = value

    def set_height(self, value):
        self.height = value

    def area(self):
        return self.width * self.height


class Square(Rectangle):
    # A square forces both sides to stay equal
    def set_width(self, value):
        self.width = value
        self.height = value  # quietly changes height too!

    def set_height(self, value):
        self.width = value
        self.height = value

On its own, that code looks reasonable — even tidy. The trouble appears the moment some other piece of code, written only to understand “Rectangle,” gets handed a Square without knowing it:

python — where the surprise happens
def describe_area(shape):
    shape.set_width(4)
    shape.set_height(5)
    # A caller who only knows about Rectangle expects area == 20
    print("Area:", shape.area())

describe_area(Rectangle(0, 0))   # prints: Area: 20 — as expected
describe_area(Square(0, 0))      # prints: Area: 25 — not what anyone expected!
Rectangle 4 × 5 = 20 matches expectation ✓ Square 4 × 4 = 16 not 20 — silent surprise ✗
Setting width then height on a Rectangle behaves predictably. Doing the exact same thing to a Square quietly changes the result.

Nothing here crashes. No error message appears. The program keeps running happily, just with the wrong number quietly baked into it — which is exactly what makes LSP violations so dangerous. They rarely announce themselves; they just slowly produce results nobody trusts anymore.

The deeper lesson isn’t “never model a Square as a kind of Rectangle.” It’s that real-world “is-a” relationships and code “is-a” relationships aren’t automatically the same thing. A square is a rectangle in geometry class. In a codebase where Rectangle promises “width and height can be set independently,” a Square simply cannot honestly keep that promise — so it shouldn’t inherit from a class that makes it.

It’s also worth noticing what this example does not say. It doesn’t say inheritance is dangerous, or that squares and rectangles can never be related in code at all. It says that the specific promise “width and height are independent” is one that a Square, by its very geometric nature, is unable to honour — so that particular promise should never have been placed in a shared parent class both types inherit from. Recognising which promises a type can genuinely make, before writing a single line of inheritance, is the entire skill LSP is trying to teach.

08

More Everyday Violations

The rectangle-square trap isn’t a one-off oddity — the same shape of mistake shows up constantly across completely different kinds of software, usually because a subclass was created purely because it “felt” related, without checking whether it could truly keep every promise.

Birds

The bird that can’t fly

A Bird class promises a fly() method. A Penguin “is a” bird zoologically, but forcing it to inherit fly() means it must either do nothing or throw an error — both break the promise.

Collections

The list that refuses to grow

A List type promises an add() method. A read-only or fixed-size list that inherits from it must throw an “unsupported” error the instant add() is called — a broken postcondition.

Payments

The card that needs extra steps

A CreditCard type promises charge(amount) works immediately. A subtype requiring a separate manual approval step before any charge can complete has quietly changed the rules.

Vehicles

The car with no engine

A Vehicle class promises startEngine(). An ElectricVehicle that has no traditional engine either fakes the method or throws — a sign the abstraction itself was drawn incorrectly.

Files

The file you can’t actually write to

A File type promises a write() method. A read-only file — one loaded from a locked archive, say — that inherits it must either silently ignore the write or throw, breaking whatever code trusted the promise.

It’s worth pausing on why this keeps happening even to careful, experienced engineers. Inheritance is genuinely tempting because it saves typing — reuse a class, override one or two methods, and move on. The trap is that this convenience is measured in the moment of writing the code, while the cost of a broken promise is paid much later, by a completely different engineer, in a completely different part of the codebase, who has no idea the shortcut was ever taken. That delay between cause and effect is exactly what makes LSP violations feel almost invisible until they cause a real, confusing bug in production.

python — the bird problem in code
class Bird:
    def fly(self):
        return "Soaring through the sky!"

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

def send_bird_south(bird):
    # Written to trust every Bird can fly. Crashes the instant
    # someone passes a Penguin — a silent trap for whoever calls this later.
    print(bird.fly())

Notice the pattern repeating across every single example: the subclass either does less than promised, throws an error the parent never mentioned, or requires an extra condition the caller was never told about. Every one of those is, at its heart, the same broken contract described back in chapter five — it just wears a different costume each time.

09

How to Spot a Violation

LSP violations rarely announce themselves with a compiler error, so engineers learn to recognise a handful of warning signs instead — little code smells that almost always point back to a broken substitutability promise.

  • Type-checking before calling a method — code that asks “is this actually a Penguin?” before deciding whether it’s safe to call a method, instead of just trusting the base type.
  • Methods that throw “not supported” errors — a strong sign the subclass was forced to inherit something it was never truly able to do.
  • Empty or do-nothing overrides — a method that exists only to satisfy the compiler, without doing anything meaningful.
  • Overridden methods with unexpected side effects — like the Square example, where setting one value silently changes another.
  • Comments warning “don’t use this subclass here” — one of the clearest possible signals that trust in the hierarchy has already broken down.
!
Watch Out For

Any method override that starts with a defensive check like “if this isn’t the right kind of object, stop here.” A base type’s whole purpose is to be trusted blindly by the code that uses it — the instant you need a safety check before trusting it, the substitution promise has already been broken.

Automated tests can help too. A well-known technique is to write one shared set of tests against the base type’s promises, then run that exact same test suite against every single subclass. Any subclass that fails tests the base type passes has, by definition, violated LSP — no manual code review even required.

Code review is still worth its weight in gold here, because some static-analysis tools can flag obvious signature mismatches but very few can reliably catch a behavioural surprise on their own. A reviewer who simply asks, out loud, “if I swapped this new subclass into every place the base type is currently used, would anything break?” catches more real LSP problems than most automated tooling ever will. It’s a cheap question to ask, and it’s worth asking every time a new subclass is added to an existing hierarchy, not just when something has already gone wrong.

10

How to Fix It

The good news is that LSP violations are almost always fixable, and usually the fix makes the whole design simpler and more honest, not more complicated. Here are the techniques engineers reach for most often, roughly in the order they’re usually tried:

1

Split the abstraction

If Bird promises fly() but not every bird can fly, create a narrower FlyingBird type. Penguin inherits only from Bird, never from FlyingBird — no broken promise required.

2

Favour composition over inheritance

Instead of Square inheriting from Rectangle, give both classes their own independent implementation, or have Square hold a Rectangle internally rather than pretending to be one.

3

Shrink fat interfaces

If a base type bundles together many unrelated abilities, break it into smaller, focused interfaces so subtypes only ever promise what they can truly deliver.

4

Write shared contract tests

Create one reusable test suite that checks the base type’s promises, and run it against every subclass automatically, catching future violations before they ship.

python — fixing the bird problem
class Bird:
    def eat(self):
        return "Pecking at some seeds."

class FlyingBird(Bird):
    def fly(self):
        return "Soaring through the sky!"

class Sparrow(FlyingBird):
    pass

class Penguin(Bird):
    def swim(self):
        return "Gliding through icy water."

# Now send_bird_south() only ever accepts FlyingBird —
# a Penguin can never be handed to it by mistake.

When Inheritance Still Works

  • All subtypes genuinely share the exact same behaviour promises.
  • The hierarchy is shallow and unlikely to grow many odd exceptions.
  • Behaviour, not just structure, is truly shared.

When to Reach for Composition

  • Only some subtypes can honour every promise the parent makes.
  • You find yourself adding “except when…” comments.
  • New subtypes keep needing to override core behaviour entirely.

None of these fixes are free — splitting an abstraction means slightly more code to read, and composition sometimes means writing a little boilerplate to forward calls from one object to another. That’s a fair trade. A little extra structure paid upfront is almost always cheaper than the alternative: a hierarchy full of silent surprises that engineers learn to work around instead of trust, one defensive type-check at a time.

11

LSP and Its SOLID Siblings

LSP rarely works alone — it leans on, and is leaned on by, the other four SOLID principles. Understanding these connections makes each principle easier to remember, because they start reinforcing each other instead of feeling like five separate rules to memorise.

OCP

Open/Closed Principle

OCP says you should extend behaviour by adding new subclasses rather than editing old code. That only stays safe if every new subclass truly honours LSP — otherwise “extending” secretly breaks things.

SRP

Single Responsibility Principle

A base class that’s trying to do too many unrelated jobs is far more likely to make promises some subclasses simply can’t keep, which is often where LSP violations are first born.

ISP

Interface Segregation Principle

Fat interfaces force subclasses to implement methods they don’t need — a direct cause of the “do-nothing” and “not supported” overrides that break substitutability.

DIP

Dependency Inversion Principle

Code that depends on abstractions rather than concrete classes only benefits fully if those abstractions can be trusted — which is exactly the guarantee LSP is meant to provide.

A useful way to remember the relationship: SRP and ISP help you draw honest abstractions in the first place, and LSP is the rule that checks whether those abstractions are actually being respected once subclasses start appearing. OCP is the reward you get for getting all of this right — a codebase you can extend confidently instead of nervously.

12

Common Pitfalls

A few failure modes show up over and over again the moment a team first tries to apply LSP seriously. Recognising them early is much cheaper than diagnosing them after they’ve caused a production bug.

Trusting the Compiler Too Much

Because signature rules are automatically checked, it’s easy to assume “my code compiles, so LSP must be fine.” As the Rectangle-Square example showed, behavioural violations sail straight past any compiler, completely unnoticed, every single time.

Modelling Real-World Categories Too Literally

Just because something is true in biology, geometry, or everyday language doesn’t mean it should become an inheritance relationship in code. A square being a rectangle, or a penguin being a bird, is true in the real world — but code hierarchies should be built around shared behaviour, not shared vocabulary.

Avoiding Composition Out of Habit

Some engineers reach for inheritance purely because it’s the tool they learned first, even in cases where composition would model the relationship far more honestly. It’s worth remembering that “Square has-a Rectangle-like calculation” is often a truer statement about the code than “Square is-a Rectangle” — and reaching for composition isn’t a lesser choice, it’s frequently the more accurate one.

Fixing Violations With More Type-Checks

A common but unhealthy patch is adding “if this is actually a Penguin, skip this part” checks scattered throughout the codebase. This doesn’t fix the violation — it just hides it, and every new subclass added later has to remember to add its own special-case check too.

!
Watch Out For

The temptation to “just add one more if-statement” instead of fixing the abstraction itself. Each patch like this makes the next one more likely, until the codebase is held together by dozens of scattered special cases nobody fully remembers the reason for.

13

Frequently Asked Questions

A handful of questions come up almost every time a team first starts using LSP as a serious design tool. Getting straight answers on them early keeps the conversation focused on real design decisions rather than terminology arguments.

Does LSP only apply to inheritance?

Not strictly. LSP is really about subtyping — the promise that one type can stand in for another. Most of the time that relationship comes from class inheritance, but it can equally apply when a class implements an interface, or even in languages that don’t use classes at all. Wherever one piece of code promises to behave like another, LSP’s rules about honouring that promise still apply.

Can a tool or compiler catch every violation automatically?

No, and this trips up a lot of newcomers. Signature-level problems — wrong parameter types, missing methods — are usually caught instantly by the compiler. But behavioural problems, like a method quietly returning a different result than expected, require careful design, code review, and well-written tests. There is no button you can press that guarantees LSP compliance.

Is inheritance always the wrong tool?

Not at all — inheritance is a perfectly good tool when every subtype genuinely can honour every promise the parent type makes, without exception. The problem only shows up when a team reaches for inheritance purely because two things sound related in everyday language, without checking whether their behaviour truly lines up.

What’s the difference between LSP and simple polymorphism?

Polymorphism is the general ability for different types to be used through a common interface — it’s a feature most modern programming languages simply provide for free. LSP is a design discipline layered on top of that feature: it’s the promise that using polymorphism this way won’t quietly break your program’s correctness. You can have polymorphism without LSP; you just end up with polymorphism you can’t fully trust.

How strict should a team be about this in practice?

Treat LSP as a strong default, not an unbreakable law enforced at gunpoint. In small, low-stakes scripts, a minor violation might genuinely not matter. But in any codebase shared by more than one engineer, or expected to grow subclasses over time, taking a few extra minutes to check substitutability during design almost always pays for itself many times over.

14

Key Takeaways

Everything above collapses neatly into a handful of durable ideas — the ones worth carrying into any future project where inheritance and subtyping are on the table.

Remember This

  • Substitutability is the whole point. The Liskov Substitution Principle says that a subtype must be safely swappable for its base type, without the surrounding code needing any special checks.
  • Every type signs a contract. Preconditions, postconditions, and invariants — a subclass must never tighten preconditions, weaken postconditions, or break an invariant.
  • Compilers catch half. Signature rules are usually caught by the compiler; behavioural rules are not — and that’s exactly where most real-world violations hide.
  • “Is-a” is not automatic. The classic Rectangle-Square example shows that a real-world “is-a” relationship doesn’t automatically mean a safe code inheritance relationship.
  • Learn the smells. Warning signs include type-checks before calling methods, “not supported” errors, do-nothing overrides, and comments warning against using a certain subclass.
  • Fix the design, not the symptom. Fixes usually involve splitting abstractions, favouring composition, shrinking fat interfaces, or writing shared contract tests — not adding more special-case checks.
  • SOLID is a system. LSP works hand-in-hand with SRP, OCP, ISP, and DIP — following one makes the others noticeably easier to follow too.

Leave a Reply

Your email address will not be published. Required fields are marked *