Course contents

Open/Closed Principle

Sign in to run this lab and save your progress

Learn

Before you start

Open/Closed is the SOLID principle that pays off the most in design interviews. Interviewers add new requirements while you're working. Candidates whose code is closed for modification handle those requirements with a smile. Candidates whose code is open for modification start rewriting what they already have and hoping nothing breaks.

The exact definition

Bertrand Meyer's original words from 1988:

"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."

Open for extension — you can add new behavior. Closed for modification — you don't have to edit existing code to do it.

Polymorphism (you learned it in module 1) is what makes both true at once. Every "new behavior" is a new subclass. Existing classes stay untouched.

What if/elif chains cost you over time

Watch what happens in a real codebase using if/elif on type:

  • Year 1: One if/elif branch, three cases. Fine.
  • Year 2: Nine cases across six methods. Copied everywhere.
  • Year 3: You need to change how "student" works. You have to find every place that mentions "student" and update them all consistently. Miss one → subtle bug.
  • Year 4: A junior adds "faculty" but forgets to update the export function. Broken CSV file in production.

Polymorphism turns "the student behavior is scattered across six files" into "the student behavior is on StudentDiscount". One class, one place to change.

The interview stress test

Watch for this in every design interview:

  • Minute 15 — "Design a discount system for our e-commerce app"
  • Minute 30 — you've drawn Regular / Student / Senior classes
  • Minute 32 — "What about a Platinum tier at 30% off?"

If your answer is "add a new class, no existing code changes", you're showing OCP fluency. If your answer is "let me edit the calculator to add another case", you're failing an invisible test.

Every design problem has this shape. Parking lot fees, vending machine categories, notification channels, chess piece moves — all of them get "add a new subtype" requirements. All of them have the same right answer.

About the AST test

The last test in this lesson reads your DiscountCalculator.apply method's source code and rejects it if it contains isinstance() or type(). Not because those functions are bad in general — but in this specific spot, using them means dispatching on type, which is exactly the smell OCP is meant to remove.

It's a strict version of the "did you actually fix the problem?" check.

Your task

O in SOLID. Bertrand Meyer's version: "Software entities should be open for extension, but closed for modification."

Translation: adding new behavior should NOT mean editing code that's already working. You add behavior by writing a new class, not by patching an old one.

The smell: long if/elif chains

Here's a discount calculator that breaks this rule:

class DiscountCalculator:
    def apply(self, customer_type, amount):
        if customer_type == "regular":
            return amount
        elif customer_type == "student":
            return amount * 0.90       # 10% off
        elif customer_type == "senior":
            return amount * 0.85       # 15% off
        elif customer_type == "loyal":
            return amount * 0.80       # 20% off
        else:
            raise ValueError(f"unknown customer type {customer_type}")

Business says "let's add a PLATINUM tier at 30% off". You open DiscountCalculator, add another elif, and ship. Along the way you accidentally break the loyal test. Every new customer type means editing this same function. Every edit puts every existing case at risk.

The fix: use polymorphism

Instead of if/elif, give each discount type its own class:

class Discount(ABC):
    @abstractmethod
    def apply(self, amount): ...

class RegularDiscount(Discount):
    def apply(self, amount): return amount

class StudentDiscount(Discount):
    def apply(self, amount): return amount * 0.90

class SeniorDiscount(Discount):
    def apply(self, amount): return amount * 0.85

class LoyalDiscount(Discount):
    def apply(self, amount): return amount * 0.80

class DiscountCalculator:
    def apply(self, discount, amount):
        return discount.apply(amount)

Now the business wants Platinum? Add a new class:

class PlatinumDiscount(Discount):
    def apply(self, amount): return amount * 0.70

Zero existing code changes. DiscountCalculator doesn't get touched. Every existing test still passes. The risk stays inside the new class.

Why this shows up in every design problem

Every real problem grows new subtypes over time:

  • Parking Lot → new vehicle types (electric bike, delivery van)
  • Vending Machine → new snack categories (frozen, cold drink)
  • Notifications → new channels (Slack, WhatsApp, WebPush)

Interviewers often ask "how would you add X?" as a stress test. The OCP answer is one line ("add a new subclass"). The if/elif answer requires you to walk through every existing case to prove nothing broke.

Your task

Build the OCP-friendly version in main.py:

  1. Abstract Discount(ABC) with abstract apply(self, amount) returning a float.
  2. Four concrete discounts:
    • RegularDiscount — returns amount unchanged
    • StudentDiscount — 10% off (amount * 0.90)
    • SeniorDiscount — 15% off (amount * 0.85)
    • LoyalDiscount — 20% off (amount * 0.80)
  3. DiscountCalculator with apply(self, discount, amount) that delegates to discount.apply(amount).

The extension test defines a new PlatinumDiscount inline (30% off) and verifies the calculator works with it — proving no existing code had to change to accommodate the new type.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
02-open-closed-starter