Loading episodes…
0:00 0:00

Master the Open/Closed Principle: Write Scalable Python Code That Lasts

00:00
BACK TO HOME

Master the Open/Closed Principle: Write Scalable Python Code That Lasts

10xTeam November 20, 2025 9 min read

The Open/Closed Principle (OCP) is the “O” in the SOLID acronym. It’s a foundational concept in software design that guides you toward creating systems that are both maintainable and scalable.

The formal definition is:

[!NOTE] Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

In simpler terms, this means you should be able to add new functionality to your system without changing existing, tested code. By closing your core logic to modification, you prevent the introduction of new bugs into stable code. By keeping it open to extension, you ensure your system can evolve with new business requirements.

The Problem: Code That Fights Back

Imagine you’re asked to build a discount calculation service. The initial requirements are simple:

  • Regular customers get a 5% discount.
  • VIP customers get a 10% discount.

A straightforward approach might lead to code like this:

# discount_service_v1.py

class DiscountService:
    def calculate_discount(self, customer_type: str, total_price: float) -> float:
        if customer_type == "Regular":
            return total_price * 0.05
        elif customer_type == "VIP":
            return total_price * 0.10
        return 0.0

This code works perfectly for the current requirements. It’s tested, deployed, and everyone is happy.

But then, the business comes back with a new request: “We need to add a Premium customer tier with a 15% discount and an Enterprise tier with a 20% discount.”

What’s the first instinct? Modify the existing calculate_discount method.

# discount_service_v2.py (The Wrong Way)

class DiscountService:
    def calculate_discount(self, customer_type: str, total_price: float) -> float:
        if customer_type == "Regular":
            return total_price * 0.05
        elif customer_type == "VIP":
            return total_price * 0.10
+       elif customer_type == "Premium":
+           return total_price * 0.15
+       elif customer_type == "Enterprise":
+           return total_price * 0.20
        return 0.0

This modification, while seemingly small, violates the Open/Closed Principle. You’ve just altered a piece of code that was already working and tested. This introduces risks:

  1. Regression Bugs: A small typo or logic error could break the discounts for Regular or VIP customers.
  2. Code Bloat: As more customer types are added, this if/elif chain will grow, making the method difficult to read and maintain.
  3. Rigidity: The system is rigid. Every new requirement forces a change in the core logic.

A Visual Metaphor: Building a House

Think of your code as a house.

graph TD;
    subgraph A[Violating OCP: The Painful Renovation]
        direction LR
        A1[House v1] -->|"Need a new floor"| A2(Demolish Roof);
        A2 --> A3(Rebuild Structure);
        A3 --> A4[House v2 with Risks];
    end

    subgraph B[Following OCP: The Seamless Extension]
        direction LR
        B1[House v1 with Flat Roof] -->|"Need a new floor"| B2(Build New Floor on Top);
        B2 --> B3[House v2 - Solid & Extended];
    end

    style A fill:#fde0e0,stroke:#c00,stroke-width:2px
    style B fill:#e0f2de,stroke:#0a0,stroke-width:2px

The first approach is like tearing down the roof of your finished house just to add another floor. It’s messy, risky, and might damage the existing structure. The second approach is like having designed the house with a flat, extendable roof from the start. You can add new floors without touching the levels below. This is the essence of OCP.

The Solution: Abstraction and Polymorphism

To adhere to the Open/Closed Principle, we need to refactor our design. Instead of a single class with conditional logic, we’ll use abstraction to define a “contract” and create multiple concrete classes that fulfill it. This is a classic application of the Strategy Design Pattern.

[!TIP] The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm at runtime. By encapsulating algorithms into separate classes, it lets you swap them interchangeably, which aligns perfectly with the Open/Closed Principle.

Let’s structure our project for scalability.

discount_project/
├── strategies.py
└── calculator.py

Step 1: Define the Abstract Strategy

First, we create an abstract base class (ABC) that defines the contract for any discount strategy.

# strategies.py
from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
    """
    The Abstract Base Class for a discount strategy.
    This defines the 'contract' that all concrete strategies must follow.
    """
    @abstractmethod
    def calculate(self, total_price: float) -> float:
        """Calculates the discount amount."""
        pass

Step 2: Create Concrete Strategy Implementations

Next, we create a separate class for each discount type. Each class inherits from our DiscountStrategy and provides its own implementation of the calculate method.

# strategies.py (continued)

class RegularDiscount(DiscountStrategy):
    """5% discount for Regular customers."""
    def calculate(self, total_price: float) -> float:
        return total_price * 0.05

class VIPDiscount(DiscountStrategy):
    """10% discount for VIP customers."""
    def calculate(self, total_price: float) -> float:
        return total_price * 0.10

# ... and so on for any other types.

Step 3: Refactor the Calculator

Now, our main DiscountCalculator becomes incredibly simple. Instead of containing logic, it delegates the calculation to a strategy object that it receives. This is a form of Dependency Injection.

# calculator.py
from strategies import DiscountStrategy

class DiscountCalculator:
    """
    This class is now closed for modification but open for extension.
    It doesn't know the details of any specific discount, it just
    uses the strategy it's given.
    """
    def calculate(self, strategy: DiscountStrategy, total_price: float) -> float:
        return strategy.calculate(total_price)

Let’s visualize the new, improved class structure.

classDiagram
    direction BT
    class DiscountStrategy {
        <<abstract>>
        +calculate(total_price) float
    }
    class RegularDiscount {
        +calculate(total_price) float
    }
    class VIPDiscount {
        +calculate(total_price) float
    }
    class PremiumDiscount {
        +calculate(total_price) float
    }
    class EnterpriseDiscount {
        +calculate(total_price) float
    }
    class DiscountCalculator {
        +calculate(strategy, total_price) float
    }

    RegularDiscount --|> DiscountStrategy
    VIPDiscount --|> DiscountStrategy
    PremiumDiscount --|> DiscountStrategy
    EnterpriseDiscount --|> DiscountStrategy
    DiscountCalculator ..> DiscountStrategy : uses

The Payoff: Effortless Extension

Now, when the business asks for the Premium and Enterprise discounts, the process is clean and safe:

  1. Create New Strategy Classes: We simply add new files or classes without touching any existing code.

    # strategies.py (adding new strategies)
    
    class PremiumDiscount(DiscountStrategy):
        """15% discount for Premium customers."""
        def calculate(self, total_price: float) -> float:
            return total_price * 0.15
    
    class EnterpriseDiscount(DiscountStrategy):
        """20% discount for Enterprise customers."""
        def calculate(self, total_price: float) -> float:
            return total_price * 0.20
    
  2. Use the New Strategies: The DiscountCalculator works with them immediately, no changes required.

    # main.py (example usage)
    from calculator import DiscountCalculator
    from strategies import RegularDiscount, VIPDiscount, PremiumDiscount, EnterpriseDiscount
    
    calculator = DiscountCalculator()
    price = 1000.0
    
    # Using the original strategies
    regular_strategy = RegularDiscount()
    vip_strategy = VIPDiscount()
    print(f"Regular Discount: {calculator.calculate(regular_strategy, price)}") # 50.0
    print(f"VIP Discount: {calculator.calculate(vip_strategy, price)}")       # 100.0
    
    # Using the NEW strategies without any changes to the calculator
    premium_strategy = PremiumDiscount()
    enterprise_strategy = EnterpriseDiscount()
    print(f"Premium Discount: {calculator.calculate(premium_strategy, price)}")     # 150.0
    print(f"Enterprise Discount: {calculator.calculate(enterprise_strategy, price)}") # 200.0
    

We have successfully extended the system’s functionality without modifying its core components. The DiscountCalculator is closed for modification, but the system is open to extension through the creation of new DiscountStrategy classes.

Deep Dive: Using a Factory to Select Strategies In a real application, you'd likely select the strategy based on a string or enum. A factory function is a perfect way to encapsulate this logic, keeping your main code even cleaner. ```python # strategy_factory.py from strategies import ( DiscountStrategy, RegularDiscount, VIPDiscount, PremiumDiscount, EnterpriseDiscount ) # A mapping from customer type string to the strategy class STRATEGY_MAP = { "Regular": RegularDiscount, "VIP": VIPDiscount, "Premium": PremiumDiscount, "Enterprise": EnterpriseDiscount, } class NoDiscount(DiscountStrategy): """A default strategy for unknown types.""" def calculate(self, total_price: float) -> float: return 0.0 def get_strategy(customer_type: str) -> DiscountStrategy: """Factory function to get the correct strategy instance.""" StrategyClass = STRATEGY_MAP.get(customer_type, NoDiscount) return StrategyClass() # --- Usage --- # calculator = DiscountCalculator() # customer_type = "Premium" # strategy = get_strategy(customer_type) # discount = calculator.calculate(strategy, 1000.0) # 150.0 ``` When you add a new discount type, you only need to add the new class and update the `STRATEGY_MAP`. The rest of the system remains untouched.

Conclusion

The Open/Closed Principle is a powerful mindset that pushes you towards creating flexible, modular, and resilient software. By favoring composition and abstraction over modification, you build systems that welcome change rather than resist it. Your future self—and your team—will thank you for it.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?