Open/Closed Principle

RoadmapsPython

Overview

The `Open/Closed Principle` (`OCP`) states that software entities should be open for extension, but closed for modification. You should be able to add new functionality without changing existing code. In Python, this is typically achieved using: - `Inheritance`: Extending base classes. - `Composition`: Injecting different behaviors. - `Duck typing`: Determining object suitability by its methods rather than its type. For example, instead of a single function checking types: def process(item): if isinstance(item, Text): item.read() elif isinstance(item, Image): item.render() You rely on a common interface: def process(item): item.handle() Now, adding a `Video` class doesn't require modifying `process`. Think of a power strip: it is closed for modification (you don't rewire it to plug in a new device) but open for extension (you just plug a new adapter into an existing socket). class Video: pass # Forgot to implement handle() def process(item): item.handle() # Traps! Raises AttributeError process(Video()) Because Python uses duck typing, extending functionality without a strict interface risks an `AttributeError` at runtime if the new class forgets a required method. Using `abc.ABC` or `typing.Protocol` is the standard way to enforce these contracts (covered fully in protocols).

It prevents bugs from being introduced into tested, working code when requirements change. It also reduces coupling and makes codebases easier to maintain as they grow.

Where used: `FastAPI` dependencies, `Django` views, Plugin architectures

Why learn this

Code walkthrough

class Logger:
  def log(self, message):
    print(f'LOG: {message}')

class App:
  def __init__(self, logger: Logger):
    self.logger = logger
  def do_work(self):
    self.logger.log('Work done')

app = App(Logger())
app.do_work()

Focus: self.logger = logger

Aha moment

class Formatter:
  def format(self, text):
    return text

class UpperFormatter(Formatter):
  def format(self, text):
    return text.upper()

def print_message(text, formatter=Formatter()):
  print(formatter.format(text))

print_message('hello', UpperFormatter())

Prediction: What will `print_message` output?

Common guess: `'hello'`

Because we passed `UpperFormatter()`, the function delegates to it, resulting in `'HELLO'`. We changed the function's behavior without altering its source code.

Common mistakes

Glossary

coupling
The degree to which different parts of a program rely on or are connected to each other; lower is usually better. Example: `process(MyClass())`.
Strategy and Factory
Common design patterns; `Strategy` lets you swap out behaviors on the fly, while `Factory` helps create new objects without exposing construction logic. Example: `EmailNotifier()`.

Recall questions

Understanding checks

In a notification system, what happens to the core `NotificationService` when a new `SlackNotification` class is added in an `OCP`-compliant design?

The core `NotificationService` does not need to be modified at all. Because new notification types implement the same shared interface, they plug in as extensions rather than requiring changes to existing tested code.

Because the system is "closed for modification" but "open for extension". The `NotificationService` relies on a shared interface. Adding a new `SlackNotification` simply extends the system by introducing a new adapter, without touching the existing, tested service code.

A developer created this code to process different file types. Why does it violate the `Open/Closed Principle`?

It uses an `if-elif` chain that checks `file_obj.type`. Adding any new file type requires editing the `process_file` function itself, which violates `OCP`'s rule that existing code should not need to change.

To support a new file type (like `xml`), you must modify the `process_file` function itself. This violates `OCP`, which states that code should be closed for modification. Instead, this should be refactored to use polymorphism, where each file object shares a common `parse()` interface.

Practice tasks

Refactor to OCP

You have a `DiscountCalculator` with an `if type == 'percent': ... elif type == 'fixed': ...` block. Refactor this to use OCP by creating a base `Discount` class and specific discount subclasses.

Challenge

Implement an OCP-compliant Notification System

Create a `Notifier` base class with a `send(message)` method. Then create `EmailNotifier` and `SMSNotifier` subclasses. Finally, write a `SystemAlert` class that accepts a list of notifiers and sends an alert message to all of them.

Continue learning

Previous: Class Creation and Instantiation

Previous: Single inheritance and super()

Return to Python Roadmap