Skip to main content

Command Palette

Search for a command to run...

SOLID Principles

Published
6 min readView as Markdown
SOLID Principles

When we start learning Object-Oriented Programming, we often just learn classes, objects, and inheritance. But when we start working on real projects, we realize that writing code that works is not enough — it should be easy to change, easy to extend, and easy to understand.

That’s where SOLID Principles come in.
They are like the “good habits” of programming.

In this post, let’s not just list definitions — we’ll think, reason, and see these principles in action with simple Python examples and real-life parallels.

What is SOLID

SOLID is an acronym for five design principles:

  • S — Single Responsibility Principle

  • O — Open/Closed Principle

  • L — Liskov Substitution Principle

  • I — Interface Segregation Principle

  • D — Dependency Inversion Principle

Single Responsibility Principle (SRP)

Definition:

A class should have only one reason to change.

That means: one class = one focused job.

EXAMPLE OF VIOLATION:

class Report:
    def __init__(self, data):
        self.data = data

    def calculate_statistics(self):
        # performs some analysis
        pass

    def save_to_file(self, filename):
        # writes data to file
        pass

    def send_email(self, address):
        # sends the report to someone
        pass

At first glance, this looks fine — it’s just one class doing everything about a report, right?
But think carefully: this class is doing 3 unrelated things:

  1. Data analysis

  2. File handling

  3. Emailing

Now, if tomorrow you want to change how reports are sent (e.g., switch from email to Slack), you’ll have to modify this class — even though the core report logic didn’t change.
That’s a violation of SRP.

Fixing It — The SRP Way

class Report:
    def __init__(self, data):
        self.data = data

    def calculate_statistics(self):
        # perform analysis
        pass

class FileSaver:
    def save(self, report: Report, filename: str):
        # logic to save report
        pass

class EmailSender:
    def send(self, report: Report, address: str):
        # logic to send report
        pass

Now, if you want to change how saving or emailing works, you just modify the respective class.
Each class has one clear reason to change.

How it connects to the principle:
You separated responsibilities so that each class has its own clear purpose.
Changing one part doesn’t risk breaking unrelated parts.

Think of a restaurant: The chef cooks, the waiter serves, the cashier bills.
If one person did everything, a single change (e.g., new billing system) would mess up everything else.

Open/Closed Principle (OCP)

Definition:
Classes should be open for extension, but closed for modification.

That means: you should be able to add new functionality without changing existing code.

EXAMPLE OF VIOLATION:

class PaymentProcessor:
    def process(self, method):
        if method == "credit_card":
            print("Processing credit card payment")
        elif method == "paypal":
            print("Processing PayPal payment")

Now, imagine adding UPI payments.
You’ll need to modify this class again — and again for every new payment type.

That’s against OCP, because existing code keeps changing.

Fixing It — The OCP Way

from abc import ABC, abstractmethod
class PaymentMethod(ABC):
    @abstractmethod
    def pay(self):
        pass

class CreditCard(PaymentMethod):
    def pay(self):
        print("Paid with Credit Card")

class PayPal(PaymentMethod):
    def pay(self):
        print("Paid with PayPal")

class PaymentProcessor:
    def process(self, method: PaymentMethod):
        method.pay()

You don’t touch existing code — just add a new class.
That’s extension without modification.

How it connects to the principle:
The system is stable (existing code safe) but flexible (can add new behavior).
You built a plug-in model instead of a switch-case model.

Think of your smartphone: You install new apps without rewriting Android or iOS.

Liskov Substitution Principle (LSP)

Definition:
Subclasses should be replaceable for their parent classes without breaking the program.

It sounds abstract, but here’s the essence:
If a subclass behaves differently in a way that violates the parent’s expected behavior, you break this rule.

EXAMPLE OF VIOLATION

class Bird:
    def fly(self):
        print("Flying high!")

class Ostrich(Bird):
    def fly(self):
        raise Exception("I can’t fly!")

Here, an Ostrich is a Bird, but it can’t fly.
If your code assumes all Bird objects can fly(), substituting an Ostrich will cause an error.

Fixing It — The LSP Way

class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        print("Flying high!")

class Sparrow(FlyingBird):
    pass

class Ostrich(Bird):
    def run(self):
        print("Running fast!")

Now, Ostrich doesn’t pretend to have a fly() method.
FlyingBird is a better abstraction for birds that can actually fly.

How it connects to the principle:
We refactored so that every subclass honors the expectations of its parent.
Replacing a parent with a child won’t break the system’s logic.

Think of a USB charger: Any USB-C device can plug in — if one “USB-C” device explodes when connected, it breaks the assumption.

Interface Segregation Principle (ISP)

Definition:
Don’t force a class to implement methods it doesn’t use.

Large “fat” interfaces are bad.
Clients should only know about the methods they actually need.

EXAMPLE OF VIOLATION:

from abc import ABC, abstractmethod

class MultiFunctionPrinter(ABC):
    @abstractmethod
    def print(self):
        pass

    @abstractmethod
    def scan(self):
        pass

    @abstractmethod
    def fax(self):
        pass

Now, if you build a simple Printer, you’re forced to implement scan() and fax()— even if you don’t need them.

Fixing It — The ISP Way

class Printer(ABC):
    @abstractmethod
    def print(self):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan(self):
        pass

class Fax(ABC):
    @abstractmethod
    def fax(self):
        pass

Now, classes implement only what they need:

class SimplePrinter(Printer):
    def print(self):
        print("Printing document...")

class AllInOne(Printer, Scanner, Fax):
    def print(self): ...
    def scan(self): ...
    def fax(self): ...

How it connects to the principle:
You avoided “fat interfaces” by giving smaller, focused contracts.
Each client depends only on what it uses — not unnecessary baggage.

Think of your smartphone charger: You only plug in the cable you need — you don’t attach extra unused connectors.

Dependency Inversion Principle (DIP)

Definition:
High-level modules shouldn’t depend on low-level modules.
Both should depend on abstractions.

The idea is to make components communicate via interfaces or abstract classes — not through hard-coded dependencies.

class MySQLDatabase:
    def fetch_data(self):
        print("Fetching from MySQL")

class ReportGenerator:
    def __init__(self):
        self.db = MySQLDatabase()  # tightly coupled

    def generate(self):
        self.db.fetch_data()

Now, if you want to switch to PostgreSQL, you must rewrite ReportGenerator.

Fixing It — The DIP Way

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def fetch_data(self):
        pass

class MySQLDatabase(Database):
    def fetch_data(self):
        print("Fetching from MySQL")

class PostgreSQLDatabase(Database):
    def fetch_data(self):
        print("Fetching from PostgreSQL")

class ReportGenerator:
    def __init__(self, db: Database):
        self.db = db  # depends on abstraction

    def generate(self):
        self.db.fetch_data()

Now, ReportGenerator can work with any database implementation.
It’s flexible and testable.

How it connects to the principle:
We inverted the dependency — the high-level module (ReportGenerator) depends on the abstract contract (Database), not the concrete class.

Think of a plug socket: You can plug in a fan or a phone charger — the wall doesn’t care which, because both follow the same voltage interface.


Summign Up:

PrincipleKey IdeaReal-Life Analogy
SRPOne class = one purposeChef doesn’t do billing
OCPAdd features without editing old codeInstall apps without changing OS
LSPSubclasses shouldn’t break parent behaviorAll USB-C devices should “just work”
ISPDon’t force classes to use extra methodsPlug only the cables you need
DIPDepend on abstraction, not detailsSocket works for any plug

Final Thoughts

  • SOLID isn’t about writing more classes — it’s about writing more sensible ones.

  • The principles often overlap. For example, applying SRP naturally helps with OCP and DIP.

  • When in doubt, ask yourself:

    “If I change this tomorrow, will something unrelated break?”

If the answer is “yes,” one of the SOLID principles might be calling for your attention.