Visual walkthrough — SOLID — Open - Closed Principle
We build one running example — a program that computes the area of shapes — and let it grow.
Step 1 — Start from a single working function
WHAT. We have exactly one shape — a circle — and one function that computes its area using the formula ==== (where is the circle's radius, the distance from centre to edge, and ).
WHY. Because right now this is perfect code. One shape, one branch, zero complexity. There is nothing to fix. Remember this feeling — OCP is about protecting exactly this "it works" state.
PICTURE. One input arrow enters one box, one number leaves.

Each symbol: is a fixed number the circle formula needs; is data the circle carries; the little means "multiply by itself." Nothing here depends on any other shape yet.
Step 2 — Add a second shape by editing the box
WHAT. A rectangle requirement arrives. We open area() and add a branch: check a shape.type label; if it says "rectangle", use (width times height) instead.
WHY did we reach for if? Because it is the first tool everyone knows and for two cases it genuinely is the fastest thing to write. This is not stupidity — it is the honest first move.
PICTURE. The single box has grown an internal fork. Notice the crucial thing: to add the new road, we had to cut the box open.

Term by term: is the incoming shape; is a text label we inspect at runtime; each row is one branch. The whole formula now knows about every shape — hold that thought, it's the disease.
Step 3 — Watch it rot (the third, fourth, fifth branch)
WHAT. Triangle, then trapezoid, then hexagon. Each arrives; each time we re-open area() and paste another elif.
WHY this hurts (make it precise). Let there be branches. Editing to add branch means the function must be re-read, re-reviewed, re-tested as a whole — because a typo in the new branch, or a stray edit, can silently break branches that were already working. The risk of an edit grows with the size of the thing you edit.
PICTURE. The box swells; every new arrow forces another cut through code that already passed its tests (shown cracking).

Step 4 — The key move: find what varies, name it
WHAT. We separate the stable part ("every shape can report an area") from the varying part (the specific formula). The stable part gets a name: an interface.
WHY an interface and not another if? Because an if collects all cases in one editable place (bad — stays high). An interface distributes each case into its own untouchable place (good — can hit zero). It inverts the direction of dependency: the calculator will now depend on the promise, not on the list of shapes. That inversion is Dependency Inversion.
PICTURE. One "socket" (the interface) with the word area() on it; the varying formulas float outside, waiting to plug in.

Step 5 — Give each shape its own box
WHAT. Circle gets its own class holding its own . Rectangle gets its own class holding . Each class knows only itself and nothing about the others.
WHY. Now the varying part is physically split apart. The circle's formula lives in the circle file; the rectangle's in the rectangle file. Editing the circle cannot touch the rectangle — they no longer share a box. This is exactly Replace Conditional with Polymorphism.
PICTURE. The one swollen box of Step 3 has split into small, isolated boxes, each holding one formula, each plugged into the shared socket.

from abc import ABC, abstractmethod
class Shape(ABC): # the frozen contract (the socket)
@abstractmethod
def area(self): ...
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2 # circle's answer, owned here
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h # rectangle's answer, owned hereStep 6 — Rewrite the high-level code to trust only the promise
WHAT. The old area() mega-function disappears. In its place, a tiny total(shapes) that just says: "for each shape, ask it its area, add them up." It never mentions circle or rectangle — only the Shape promise.
WHY this is the payoff. total depends only on the interface. Add a hundred new shapes and total never changes. We have driven (edits to existing code) to zero. That is the moment OCP is achieved.
PICTURE. The high-level box points only at the socket; the shapes plug into the socket from below. The high-level box and the socket are frozen (shown locked).

def total(shapes):
return sum(s.area() for s in shapes) # talks to the promise, never to a typeStep 7 — Extend without editing (the "open" half, proven)
WHAT. New requirement: triangles. We do not open total, not open Circle, not open the Shape interface. We write one new file: Triangle(Shape) with formula ( = base, = height).
WHY it just works. total loops over "things that are Shapes and can area()." A Triangle is a Shape and can area(). So it slots in with zero edits to tested code. Open for extension (new class added) and closed for modification (old files untouched) — both, at the same time, no longer a paradox.
PICTURE. A brand-new triangle box slides into the untouched socket. Everything old is greyed/locked; only the new box is coloured.

class Triangle(Shape):
def __init__(self, b, h): self.b, self.h = b, h
def area(self): return 0.5 * self.b * self.h # brand new, edits nothing elseStep 8 — Degenerate & edge cases (don't skip these)
Every real derivation must survive its corners. Here are all of them.
Recall Check yourself on the cases
Empty shape list total ::: — the sum over an empty collection is zero, no special case needed.
Area of Circle(0) ::: , a degenerate point; the calculator is unaffected.
When does the Rule of Three say to abstract ::: On the third variation, not the first — earlier abstraction is speculative over-engineering.
Does OCP block fixing a wrong formula ::: No — that's a bug fix (changing wrong behaviour), not adding new behaviour.
The one-picture summary
This single figure is the entire derivation compressed: on the left, the rotting if-chain where every new shape cuts the box open ( high, risk high); on the right, the socket-and-plugs design where new shapes plug in and old code stays locked ().

Recall Feynman: the whole walkthrough in plain words
We started with a robot that could do one trick, so we opened its head and wired the trick in — fine. Second trick: opened the head again — still fine. But by the fifth trick, opening the head kept bumping the old wires, and sometimes the waving broke while we were adding dancing. So we asked: what's the same about every trick? Every trick answers "show me your move." That question never changes — so we bolted a slot onto the robot labelled "trick." Now each trick lives on its own little card it can never disturb another. The robot's body just says "for each card, play its move." To add dancing we slide in a new dance card — we never open the head again. Old code stays closed; new tricks just plug in. The reason this beats the if-chain is pure arithmetic: risk grows with how big a thing you keep re-opening, and now we re-open nothing — so risk drops to zero for every future trick.
Connections
- Yeh walkthrough Hinglish mein
- Polymorphism — the language feature that makes "ask each shape its own
area()" possible. - SOLID — Dependency Inversion Principle — why
totalpoints at the interface, not the shapes. - Strategy Pattern — the same socket-and-plug shape applied to algorithms instead of shapes.
- Template Method Pattern — a sibling realization that fixes the skeleton, varies the steps.
- Refactoring — Replace Conditional with Polymorphism — the exact Step 3 → Step 5 move.
- SOLID — Single Responsibility Principle — why each shape owning one formula feels clean.
- SOLID — Liskov Substitution Principle — why any
Shapemay safely stand in the socket.