What Is the Open/Closed Principle?
Add new behaviour without touching what already works. That is the whole promise of this principle — and once you see how it is done, you will start noticing broken versions of it everywhere.
The Big Idea, in One Breath
Well-designed code should be open to new behaviour being added, but closed against having its existing, working parts rewritten every time something new needs to be supported.
Imagine a power strip sitting against your bedroom wall. Whenever you get a new gadget — a lamp, a phone charger, a fan — you simply plug it into one of the open sockets. Nobody ever needs to unscrew the power strip and rewire its insides just to make room for a new device. The power strip stays exactly as it was, untouched and trustworthy, while still happily accepting new things plugged into it.
That is the entire spirit behind the Open/Closed Principle, the second of the five SOLID principles in object-oriented design. It states that well-designed code should be open to new behaviour being added, but closed against having its existing, working parts rewritten every time something new needs to be supported.
Think about a train track. New train cars can be added to the back of a train without anyone needing to redesign the engine or the tracks already laid down. The track system was built with a standard coupling mechanism in mind — new cars simply hook on, using that same standard connector, without disturbing anything that came before.
This guide continues a series exploring object-oriented design, following earlier guides on the four pillars of OOP, inheritance, polymorphism, abstract classes versus interfaces, an overview of all five SOLID principles, and a deep dive into the Single Responsibility Principle. This particular guide zooms in on the second letter — the “O” in SOLID — which many developers consider the principle most directly responsible for keeping a growing codebase calm and predictable, rather than turning into a minefield every time a new feature request arrives.
The Formal Definition
Software entities — classes, modules, functions — should be open for extension, but closed for modification. First stated by Bertrand Meyer, later popularised as part of the SOLID acronym.
The principle is most commonly stated as: software entities — classes, modules, functions — should be open for extension, but closed for modification. This phrasing comes from Bertrand Meyer, who first described the idea, and it was later popularised further as part of the SOLID acronym by Robert C. Martin.
Two words carry all the weight in that sentence: open and closed. Together, they describe two properties that a well-designed piece of code should have at the same time — not in conflict with each other, but as two sides of the same coin.
“What does it mean for code to be open/closed?” is really asking: you should be able to teach it something new without having to reopen and re-examine what it already knew.
It is worth being precise about what “closed” does not mean here, since it is easy to misread. It does not mean a class can never, ever be edited under any circumstances for the rest of its life. It means that once a piece of code is stable and correct, growing the system to support new cases should not routinely require reopening it. Genuine bug fixes are a different matter entirely, and this guide returns to that distinction later on.
“Open” and “Closed,” Unpacked
Growth happens by addition, not by editing. Two words that sound like a contradiction, until you see the trick: designing good, stable seams where new pieces plug in cleanly.
Open for extension means it should be genuinely possible to add new behaviour to a system — a new type of shape, a new payment method, a new kind of discount — without hitting a wall that forces existing code to be rewritten.
Closed for modification means that once a piece of code is written, tested, and working, it should be able to stay that way. Other developers should not need to open it up and edit its internals every time the surrounding system grows.
Put together, “open for extension, closed for modification” describes a system where growth happens by addition, not by editing. It might sound like a contradiction at first — how can something be both open and closed at once? — but the trick, which the rest of this guide unpacks, lies in designing good, stable seams where new pieces can plug in cleanly.
It helps to picture these two words as describing two different audiences for the exact same piece of code. To someone wanting to add new behaviour, the code should feel welcoming and open — there is a clear, sanctioned place to plug something new in. To someone tempted to reach in and rewrite the existing internals just to squeeze in a new case, the code should feel closed — resistant, by design, to that kind of casual editing. A single, well-designed class can genuinely offer both experiences to these two different people at the same time.
Why It Matters
Every edit to working code carries a real risk of breaking something that already worked. This principle minimises that risk by making “add” the default way a system grows.
Working code stays working
Untouched code cannot develop new bugs — editing is where accidental breakage tends to creep in.
Old tests keep passing
If nothing existing was changed, previously passing tests have no reason to suddenly fail.
Teams grow the system side by side
Adding new, separate pieces rarely conflicts with someone else doing the same elsewhere.
Stable foundations stay trustworthy
Code nobody needs to keep re-editing earns a reputation as dependable, tested, and safe to build on.
There is a particularly compelling real-world reason this matters: every time a working piece of code gets edited, there is a genuine risk of introducing a new bug into something that used to work perfectly well. The Open/Closed Principle minimises that risk by making “adding something new” the default way a system grows, rather than “cutting into something old.”
This becomes especially valuable the larger and older a codebase gets. A class written and thoroughly tested three years ago, relied upon by dozens of other parts of the system, carries real risk every time it is reopened for editing — not because the new change is necessarily wrong, but because nobody can be fully certain they have accounted for every single thing that currently depends on its exact, existing behaviour. Code that is never touched again, once correct, simply cannot develop this particular category of risk.
How to Spot a Violation
A long chain of conditions checking a type, over and over, scattered across a codebase. Once you know what to look for, it becomes surprisingly easy to spot — even in unfamiliar code.
The most common and recognisable sign of an Open/Closed violation is a long chain of conditions checking a type, over and over, scattered across a codebase. Once you know what to look for, it becomes surprisingly easy to spot, even in unfamiliar code.
def calculate_area(shape): if shape.type == "circle": return 3.14159 * shape.radius ** 2 elif shape.type == "square": return shape.side ** 2 elif shape.type == "triangle": # someone had to add this branch return 0.5 * shape.base * shape.height
Every time a new shape needs to be supported, someone has to reopen this exact function and add another branch. Worse, if this same type-checking pattern is repeated in several other places throughout the program — drawing the shape, printing its name, calculating its perimeter — a single new shape now requires hunting down and editing every single one of those places.
Long if/elif or switch chains checking a type
Especially when the same type-check pattern repeats in multiple, separate places.
A comment saying “add new cases here”
A visible sign that a piece of code expects to be reopened and edited repeatedly, by design.
Adding a new feature always touches the same old file
If growth consistently means editing one particular, central file, that file has likely become a bottleneck.
Fear of touching a certain file
If developers on a team openly dread editing a particular class, worried it might break something unrelated, that fear itself is a strong sign the class was never properly closed against modification in the first place.
None of these signs require special tools to notice — they mostly show up simply by paying attention to how a team actually behaves around certain parts of the codebase. A recurring pattern of “ugh, not that file again” whenever a new feature request comes in is often the clearest, most honest signal available that an Open/Closed violation is quietly slowing everyone down.
A Before-and-After Example
Fix the shape example using abstraction and polymorphism. Adding a Triangle later needs zero changes anywhere else.
Let us fix the shape example from the previous section, using the tools covered in earlier guides in this series: abstraction and polymorphism. Watching the transformation step by step makes the underlying trick much easier to internalise than reading about it in the abstract.
abstract class Shape: abstract def area(self): pass class Circle(Shape): def area(self): return 3.14159 * self.radius ** 2 class Square(Shape): def area(self): return self.side ** 2 # adding Triangle later needs zero changes above class Triangle(Shape): def area(self): return 0.5 * self.base * self.height def calculate_area(shape: Shape): return shape.area() # never needs editing again
calculate_area() no longer knows or cares how many shape types exist. Every shape carries its own area calculation with it, thanks to polymorphism. Adding Triangle required creating one new class — nothing about Circle, Square, or the function that uses them needed to change at all.
It is worth tracing what happens if this same design later needs a Pentagon, or a Hexagon, or any shape nobody has thought of yet. The pattern repeats exactly the same way every time: create one new class, give it its own area() method, and everything else in the system continues working without a single edit. This is precisely what “open for extension” looks like in practice — not a promise made in the abstract, but a genuinely repeatable, mechanical pattern anyone on the team can follow confidently, again and again, as the system keeps growing.
The Mechanism Behind It
Not a separate technical tool. The natural payoff of good abstraction and polymorphism, both covered in earlier guides in this series.
It is worth being explicit about how this trick actually works, because “open and closed at the same time” can sound like magic until you see the mechanism plainly. The Open/Closed Principle is not a separate technical tool — it is the natural payoff of good abstraction and polymorphism, both covered in earlier guides in this series.
By defining a shared, stable promise — like area() — code that depends on that promise never needs to know which specific class fulfils it. New classes can fulfil that same promise in their own way, sliding in beside the old ones without disturbing anything. The “closed” part comes from depending on a stable abstraction; the “open” part comes from that abstraction being extendable by new classes.
This is exactly why the Open/Closed Principle is often introduced right after Single Responsibility in the SOLID lineup — a class with one clean job is much easier to build a stable, extendable abstraction around than a class already juggling several tangled concerns.
It is worth being honest about a subtlety here: designing a genuinely good extension point takes real skill, and it is not something that happens automatically just by adding an abstract class. The abstraction needs to capture the right underlying idea — area() works well for shapes because every shape genuinely has one, in its own way. Choosing the wrong abstraction, one that does not actually fit every future case cleanly, can quietly recreate the same rigidity this principle is meant to avoid, just hidden one layer deeper.
Common Extension Patterns
Over the years, developers have settled on a handful of recurring, well-tested ways to build genuinely good extension points — plugins and strategies chief among them.
Over the years, developers have settled on a handful of recurring, well-tested ways to build genuinely good extension points. None of these require memorising formal names to use well, but knowing they exist helps when designing a new system from scratch.
The plugin pattern
A core system defines a stable, general promise — like Shape‘s area() — and new, independent pieces can be written later that fulfil that promise, without the core system needing to know about them in advance. This is exactly how many apps support third-party extensions or add-ons.
The strategy pattern
Instead of one method containing a long chain of type-checks to decide “how” to do something, the “how” itself is handed to the code from outside, as a swappable, interchangeable object. A checkout system, for instance, might accept any object that knows how to calculate a discount, without needing to know in advance which specific discount strategy will be used.
class Checkout: def __init__(self, discount_strategy): self.discount_strategy = discount_strategy def total(self, price): return self.discount_strategy.apply(price) # New discount types can be added later without touching Checkout at all class HolidayDiscount: def apply(self, price): return price * 0.8
Checkout never needs to be edited to support a new kind of discount — it simply accepts whichever discount strategy it is handed, trusting that strategy to know how to apply itself correctly. This is the same underlying trick as the shape example, expressed in a slightly different, equally common shape.
Weighing It Fairly: Pros and Cons
Not a free, universal win. Every extension point costs a small amount of upfront design effort — paying off many times over only if it genuinely gets used.
Like every principle in this series, the Open/Closed Principle is not a free, universal win — it is a genuine trade-off, and applying it wisely means understanding both sides honestly.
Strengths
- New features can be added with far less risk to what already works.
- Existing, tested code stays untouched and trustworthy.
- Teams can extend a system in parallel without stepping on each other.
Trade-offs
- Requires upfront thought about where extension points genuinely belong.
- Over-preparing for extensions that never happen adds unnecessary complexity.
- Not every part of a system needs, or benefits from, this level of flexibility.
A helpful way to hold this trade-off in mind: every extension point costs a small amount of upfront design effort, paid once. If that extension point genuinely gets used — if new shapes, new discount types, or new payment methods really do keep showing up over the life of a project — that upfront cost pays for itself many times over. If it never gets used, the cost was paid for nothing, which is exactly why judgment about where change is actually likely matters just as much as knowing the technique itself.
Common Pitfalls to Avoid
Four recurring traps — unused extension points, confusing “closed” with “frozen forever,” guessing the wrong axis of change, and over-abstracting simple problems.
Even developers who understand the principle well can fall into a handful of recurring traps when applying it to real, evolving projects.
Adding extension points nobody will ever use
Designing elaborate, flexible extension points for a case that never actually materialises adds real complexity for no real benefit. This principle earns its value specifically where change is genuinely expected. A good habit: wait until a second, real case actually shows up before generalising — one example alone rarely reveals what the right extension point should even look like.
Confusing “closed” with “never allowed to fix a bug”
A genuine bug still deserves a fix. “Closed for modification” is about avoiding edits driven by new features, not about refusing to correct something that was wrong to begin with. Treating “closed” as an absolute, universal freeze would leave broken code broken forever, which was never the intent.
Guessing wrong about where extension will actually happen
Building flexibility around the wrong axis of change — the one that never actually gets extended — wastes effort while leaving the real, frequently-changing part just as rigid as before. It is worth pausing, before designing an extension point, to ask honestly which part of a system is truly expected to grow, rather than assuming the most obvious-looking candidate is automatically the right one.
Building too many layers of abstraction for a simple problem
Occasionally, a problem genuinely has a small, fixed number of cases that will never realistically grow. Wrapping such a case in a full abstract class and polymorphic structure, purely out of habit, adds ceremony without a matching benefit. A short, honest condition can sometimes be the more appropriate, simpler choice.
Trying to make every single class fully open and closed from day one. This principle is best applied where real, recurring change is already visible, not speculatively everywhere at once.
Where You Will Meet This Every Day
The Open/Closed Principle quietly shapes an enormous amount of the software people rely on daily, even in products that never mention SOLID at all.
The Open/Closed Principle quietly shapes an enormous amount of the software people rely on every day, even in products that never mention SOLID at all.
| Everyday Example | How OCP Shows Up |
|---|---|
| A web browser | New extensions can be installed without anyone rewriting the browser’s core. |
| A video game | New character skins or levels can often be added as content, without touching the game engine. |
| A smart home hub | New compatible devices can be added without the hub’s core software being rewritten. |
| A payment platform | New payment methods can be added as separate, self-contained pieces, without editing the core checkout flow. |
| A photo editing app | New filters can typically be added as independent plug-ins, without touching the app’s core editing engine. |
For new behaviour
New pieces slot in alongside the existing ones, using the same shared, stable connectors.
For edits to what works
Stable, tested code stays exactly as it was, spared the risk that every fresh edit inevitably carries.
The second letter of SOLID
Sitting right next to Single Responsibility, and quietly relying on its cleanliness to work well.
Outlets, shelves, train couplers
The same underlying wisdom shows up in electrical outlets, library shelving, and standardised train couplings.
It is worth noticing this same underlying pattern outside of software entirely. A standardised electrical outlet lets new appliances be added to a home’s power system without anyone rewiring the walls. A public library’s shelving system lets new books be added without redesigning the building. In every one of these cases, the underlying wisdom is identical to what this guide has been describing: define a stable, well-thought-out way for new things to connect, and growth stops requiring disruption to what is already there.
Questions People Often Ask
Six honest questions that come up over and over — and honest answers to each.
Does this mean code should never be edited, ever?
No. Bugs still get fixed, and code still gets refactored for clarity. The principle is specifically about avoiding edits to stable, working code purely to support new, unrelated behaviour — not a blanket ban on ever touching a file again.
Is a small if/else statement always a violation?
Not necessarily. A short, stable condition that is unlikely to ever grow new branches is not automatically a problem. The concern is specifically about conditions expected to keep growing every time something new is added — a fixed, two-branch check that genuinely will never need a third branch is often perfectly fine left exactly as it is.
How does this relate to the Single Responsibility Principle?
They reinforce each other closely. A class with one clean responsibility is much easier to build a stable extension point around than a class already juggling several unrelated jobs, since a tangled, multi-purpose class rarely has a single, obvious “shape” that new behaviour can cleanly extend.
Can this principle be applied to a project that already exists, or only to brand-new code?
It can absolutely be applied to existing code, and this is actually one of the more common, practical uses of the principle. The next time a long chain of type-checks needs a new branch added, that is often a natural, low-risk moment to refactor it into a more open, extendable shape instead of simply adding yet another branch to the pile.
Does following this principle guarantee a bug-free system?
No — like the other SOLID principles, it is about structure and maintainability, not a guarantee against logical mistakes. What it does offer is a meaningful reduction in a specific, common category of risk: the risk of breaking something that used to work, simply because it had to be reopened and edited to support something new.
Is it always obvious where an extension point should go?
Not always, and this is genuinely one of the harder judgment calls in software design. It often takes seeing a second or third real example of “the same kind of new thing” before the right, general shape for an extension point becomes clear. Trying to guess it perfectly from a single example is one of the more common ways this principle gets applied in the wrong place.
Key Takeaways
A simple habit worth carrying into every growing codebase: prefer adding something new over reopening something that already works.
The Open/Closed Principle, at its core, is a simple habit worth carrying into every growing codebase: prefer adding something new over reopening something that already works. It does not require special tools, only a bit of upfront thought about where a system’s real, recurring changes are likely to happen.
Remember this
- The Open/Closed Principle asks for code that is open to new behaviour, but closed against edits to what already works.
- Long chains of type-checks scattered across a codebase are the classic warning sign of a violation.
- Abstraction and polymorphism are the mechanism that actually makes “open and closed at once” possible.
- Common patterns like plugins and strategies offer well-tested ways to build genuinely good extension points.
- It is most valuable where real, recurring change is expected — not applied speculatively everywhere.
- A genuine bug still deserves a fix; “closed” refers to new feature growth, not correctness.
- The right extension point often only becomes clear after seeing a second or third real example of the same kind of change.