DEV Community

Rajesh Vemulakonda
Rajesh Vemulakonda

Posted on

Stop Editing Working Code: Understanding the Open/Closed Principle in Python

Every time a new requirement arrives, do you find yourself opening the same function and adding another elif block?

If yes, your code may be signaling a design problem.

Most software bugs aren't caused by adding new features.

They're caused by modifying existing features that were already working.

The Open/Closed Principle (OCP) is one of the most effective ways to reduce that risk.

Consider a simple export utility:

def export(data, file_type):

    if file_type == "pdf":
        export_pdf(data)

    elif file_type == "csv":
        export_csv(data)
Enter fullscreen mode Exit fullscreen mode

A few weeks later:

elif file_type == "json":
    export_json(data)
Enter fullscreen mode Exit fullscreen mode

Then:

elif file_type == "xml":
    export_xml(data)
Enter fullscreen mode Exit fullscreen mode

Then:

elif file_type == "html":
    export_html(data)
Enter fullscreen mode Exit fullscreen mode

The code still works.

The problem is that every new requirement forces us to modify existing, tested code.

As the application grows, this pattern increases:

  • Maintenance effort
  • Regression testing
  • Risk of introducing bugs

This is exactly the problem the Open/Closed Principle (OCP) tries to solve.

Software entities should be open for extension but closed for modification.

In simple terms:

Add new behavior without repeatedly editing code that already works.

Most discussions stop at inheritance and polymorphism. Python, however, provides several additional ways to achieve OCP that often feel more natural.

Let's explore them.


OCP Through Duck Typing

Many OOP examples start by creating a base class:

class Exporter:
    def export(self, data):
        pass
Enter fullscreen mode Exit fullscreen mode

and then inheriting from it.

Python often doesn't need that.

class PDFExporter:

    def export(self, data):
        print("Exporting PDF")
Enter fullscreen mode Exit fullscreen mode
class CSVExporter:

    def export(self, data):
        print("Exporting CSV")
Enter fullscreen mode Exit fullscreen mode
class JSONExporter:

    def export(self, data):
        print("Exporting JSON")
Enter fullscreen mode Exit fullscreen mode

A generic function can simply rely on behavior:

def export_data(exporter, data):
    exporter.export(data)
Enter fullscreen mode Exit fullscreen mode

Usage:

export_data(PDFExporter(), data)
export_data(CSVExporter(), data)
export_data(JSONExporter(), data)
Enter fullscreen mode Exit fullscreen mode

The function doesn't care about inheritance.

It only cares that the object provides an export() method.

Need XML support?

class XMLExporter:

    def export(self, data):
        print("Exporting XML")
Enter fullscreen mode Exit fullscreen mode

No existing code changes.

That's extension without modification.


OCP Through First-Class Functions

Python treats functions as first-class objects.

That means behavior itself can become an extension point.

def export_pdf(data):
    print("Exporting PDF")
Enter fullscreen mode Exit fullscreen mode
def export_csv(data):
    print("Exporting CSV")
Enter fullscreen mode Exit fullscreen mode
def export_json(data):
    print("Exporting JSON")
Enter fullscreen mode Exit fullscreen mode

Generic exporter:

def export_data(exporter, data):
    exporter(data)
Enter fullscreen mode Exit fullscreen mode

Usage:

export_data(export_pdf, data)
export_data(export_csv, data)
export_data(export_json, data)
Enter fullscreen mode Exit fullscreen mode

Adding support for XML becomes trivial:

def export_xml(data):
    print("Exporting XML")
Enter fullscreen mode Exit fullscreen mode
export_data(export_xml, data)
Enter fullscreen mode Exit fullscreen mode

Again, existing code remains untouched.


OCP Through Registration Decorators

As systems grow, we often need a way to discover available functionality automatically.

Decorators can act as registration mechanisms.

Start with a registry:

exporters = {}
Enter fullscreen mode Exit fullscreen mode

Create a registration decorator:

def register(file_type):

    def decorator(func):
        exporters[file_type] = func
        return func

    return decorator
Enter fullscreen mode Exit fullscreen mode

Register exporters:

@register("pdf")
def export_pdf(data):
    print("Exporting PDF")
Enter fullscreen mode Exit fullscreen mode
@register("csv")
def export_csv(data):
    print("Exporting CSV")
Enter fullscreen mode Exit fullscreen mode

Dispatcher:

def export(data, file_type):
    exporters[file_type](data)
Enter fullscreen mode Exit fullscreen mode

Adding a new format becomes:

@register("xml")
def export_xml(data):
    print("Exporting XML")
Enter fullscreen mode Exit fullscreen mode

No dispatcher changes required.

This is the foundation of many plugin-style architectures.


OCP Through functools.singledispatch

Python's standard library includes an elegant extension mechanism called singledispatch.

Without it:

def export(data):

    if isinstance(data, list):
        ...

    elif isinstance(data, dict):
        ...

    elif isinstance(data, str):
        ...
Enter fullscreen mode Exit fullscreen mode

With singledispatch:

from functools import singledispatch


@singledispatch
def export(data):
    raise TypeError("Unsupported type")
Enter fullscreen mode Exit fullscreen mode

Register implementations:

@export.register
def _(data: list):
    print("Exporting list")
Enter fullscreen mode Exit fullscreen mode
@export.register
def _(data: dict):
    print("Exporting dictionary")
Enter fullscreen mode Exit fullscreen mode
@export.register
def _(data: str):
    print("Exporting string")
Enter fullscreen mode Exit fullscreen mode

Need tuple support?

@export.register
def _(data: tuple):
    print("Exporting tuple")
Enter fullscreen mode Exit fullscreen mode

No changes to the original function.

Just register another implementation.


When OCP Becomes Overengineering

Not every problem needs abstractions.

This code is perfectly fine:

if age >= 18:
    print("Eligible")
else:
    print("Not Eligible")
Enter fullscreen mode Exit fullscreen mode

Creating:

class EligibilityStrategy:
    ...
Enter fullscreen mode Exit fullscreen mode

would likely make the code worse, not better.

The goal is not to eliminate every conditional statement.

The goal is to identify areas that genuinely evolve over time.

Examples include:

  • File exporters
  • Payment processors
  • Notification systems
  • Authentication providers
  • Data parsers

These are natural candidates for extension.


Want the Full Version?

This article focuses on Python-specific techniques for applying the Open/Closed Principle.

For a deeper dive covering:

  • Inheritance
  • Polymorphism
  • Strategy Pattern
  • Practical design trade-offs
  • A decision checklist
  • Detailed examples

see the full companion guide on GitHub:

https://github.com/Vemulakonda559/open-closed-principle-python

Key Takeaway

Many developers associate the Open/Closed Principle with inheritance.

Python offers a broader perspective.

We can achieve OCP through:

  • Duck Typing
  • First-Class Functions
  • Registration Decorators
  • functools.singledispatch

The common theme is always the same:

Add new behavior without repeatedly modifying existing behavior.

The next time you find yourself adding another branch to a growing if-elif chain, pause and ask:

Can this be extended instead?

That question is often the first step toward writing code that is easier to maintain, test, and evolve.

Top comments (0)