Visual walkthrough — SOLID — Liskov Substitution Principle
We link out to Polymorphism and Design by Contract as we go — those are the two ideas LSP protects.
Step 1 — What is a "type" from the caller's point of view?
WHAT. Forget classes for a second. A caller is any piece of code that receives an object and uses it. It doesn't inspect the object's true identity — it only trusts a promise sheet: "whatever you hand me, it can do these things, and it will behave like this."
WHY start here. Every rule we derive comes from one fact: the caller only ever reads the base's promise sheet. It never learns about the subclass. So we must picture the caller as blindfolded to the real type. If we skip this, the rules look arbitrary.
PICTURE. The caller holds a slot shaped like the base type. Any object that fits the slot can be dropped in. The caller never looks inside the box — it only reads the label on the slot.

Step 2 — A contract is two lists: what you may send, what you'll get back
WHAT. Pin down what "promise sheet" means. Every method has exactly two lists:
- Precondition — the set of inputs the caller is allowed to send.
- Postcondition — the set of guarantees the method makes about the result.
WHY these two. This is Design by Contract: a method is a deal. You (caller) hold up your side by sending legal input (satisfying the precondition). It holds up its side by returning a legal result (satisfying the postcondition). Everything about substitutability lives in these two lists — so we draw them as regions, not words.
PICTURE. Draw the precondition as a region of "allowed inputs" and the postcondition as a region of "guaranteed outputs." Bigger region = more permissive.

Step 3 — Substitution: drop the subclass into the caller's slot
WHAT. Now perform the swap LSP is named for. The caller was written for . We secretly hand it an . Nothing else changes — the caller still sends -legal inputs and still relies on -promised outputs.
WHY. This is the entire test. Polymorphism only pays off if this silent swap is invisible to the caller. So we ask: under what conditions on 's two lists does the caller notice nothing?
PICTURE. Two promise sheets overlaid — the base's (teal) and the subclass's (orange). The caller aims its calls at the teal blob but the real object reads them with the orange blob. Where the orange blob disagrees with teal, the caller gets surprised.

Everything now reduces to one question, asked twice: how must the orange (sub) blobs sit relative to the teal (base) blobs so no surprise leaks out?
Step 4 — The precondition must WIDEN (contravariance), derived
WHAT. Focus only on inputs. The caller sends any input inside — that's what the promise sheet said was allowed. The subclass actually checks against .
WHY the direction. Suppose the subclass were pickier — smaller than . Then there's an input the base said "yes" to but the subclass says "no" to. The blindfolded caller sends it → the subclass rejects/crashes → surprise. To forbid this, every base-legal input must also be sub-legal. The sub blob must contain the base blob.
PICTURE. Orange (sub) input blob must be at least as big as the teal (base) input blob. The dangerous case — a smaller orange blob leaving a red "trap zone" of inputs the caller may legally send but the sub rejects — is drawn in red.

Step 5 — The postcondition must NARROW (covariance), derived
WHAT. Now focus only on outputs. The caller trusts every guarantee in — its later code depends on the result landing inside that blob. The subclass actually delivers results inside .
WHY the direction. Suppose the subclass were sloppier — bigger than , meaning the result can land outside what the base promised. The caller's downstream code assumed it never would → surprise. To forbid this, every result the sub can produce must still satisfy the base's promise. The sub output blob must sit inside the base output blob.
PICTURE. Orange (sub) output blob must be no bigger than the teal (base) output blob. The dangerous case — an orange blob poking outside teal, a red "broken-promise" zone — is drawn in red.

Step 6 — Edge case: the subclass throws a NEW exception (rule 3)
WHAT. A subtly different failure. The Penguin from the parent note doesn't shrink its input blob or grow its output blob — it escapes the diagram entirely by throwing UnsupportedOperationException from fly().
WHY it's still a violation. An exception is a third exit the promise sheet never mentioned. The caller of Bird.fly() proved "this returns normally." The Penguin makes that proof false. In blob terms: the result lands in no output blob at all — it leaves through a door the base sheet didn't draw.
PICTURE. The base has one exit (normal return). The subclass sprouts a second, unlabeled exit (the thrown exception). The caller, holding only the base sheet, never built a catch for it → crash.

Step 7 — Degenerate case: the perfect (trivial) substitute
WHAT. Check the boundary where both rules hold exactly: the subclass copies the base's two blobs unchanged (adds no behaviour, overrides nothing meaningfully).
WHY show it. A rule you can't satisfy is useless. This case proves the region conditions are achievable — the base type is trivially its own valid subtype, which is the sanity anchor. It also shows the "just right" fit: equal blobs satisfy both (inputs) and (outputs) at once.
PICTURE. Orange and teal blobs coincide perfectly, both for inputs and outputs. No red zones anywhere. This is substitutability at equality — the calm center between "too picky" and "too sloppy."

Recall Why equality is allowed but the two arrows still differ
Equality satisfies (equal ⇒ subset) and (equal ⇒ subset) simultaneously. The arrows only forbid crossing the boundary the wrong way; sitting on the boundary is always fine. Equality is a valid substitute? ::: Yes — a base type is always its own valid subtype; equal blobs satisfy both rules.
The one-picture summary
Everything compresses into one diagram: inputs widen going down the hierarchy, outputs narrow going down, exceptions may only shrink, and any object that keeps its two blobs on the safe side is invisible to the caller — which is exactly what makes Polymorphism pay off.

Recall Feynman: the whole walkthrough in plain words
Picture a caller wearing a blindfold. It was handed a promise sheet for a base type, and it works purely off that sheet: "I'm allowed to send these inputs, I'll get back these results." Then someone quietly swaps the real object for a subclass — the blindfold stays on. For the swap to go unnoticed, the subclass must do two things. First, it must accept every input the sheet said was legal (and it's welcome to accept more) — if it got pickier, the caller would send a now-illegal input and hit a wall. Second, it must return results that stay inside what the sheet promised (and it's welcome to be even more precise) — if it got sloppier, the caller's later steps, trusting the old promise, would break. Inputs grow, outputs shrink: same rule "don't surprise the blindfolded caller," read once at the entrance and once at the exit. And it may never sneak out a new emergency exit (a fresh exception) the sheet never drew — that's the Penguin who screams "I can't fly!" when a Bird was promised. The safe, boring, perfect case is a subclass whose blobs match the base's exactly. That's LSP.