Single Responsibility Principle (SRP)

A class should have only one reason to change. It should do exactly one job.

Violates SRP

class UserService:
    def validate(self, user): pass
    def send_email(self, user): pass

Follows SRP

class UserValidator:
    def validate(self, user):
        pass
 
class EmailSender:
    def send(self, user):
        pass

Each class handles only one responsibility. Validation, email sending, and persistence are separated.

Open/Close Responsible (OCP)

Open for extension, closed for modifications

def process_payment(type):
    if type == "card":
        print("card payment")
    elif type == "upi":
        print("upi payment")
from abc import ABC, abstractmethod
 
class Payment(ABC):
    @abstractmethod
    def process(self):
        pass
 
class CardPayment(Payment):
    def process(self):
        print("paid using card")
 
class UpiPayment(Payment):
    def process(self):
        print("paid using upi")
 
 
def process(payment: Payment):
    payment.process()

To add a new payment type, I create a new class. I don’t modify existing logic. That’s OCP.

Liskov Substitution Principle (LSP)

LSP means subclasses should not break the behavior promised by the parent class. For example, a Penguin extending Bird violates LSP because it breaks the expectation that all birds can fly.

Example for LSP Violation

class Bird:
    def fly(self):
        return "flying"
 
class Sparrow(Bird):
    pass
 
class Eagle(Bird):
    pass
 
class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly")

Interface Segregation Principle (ISP)

ISP means we should split large interfaces into smaller ones so that classes only implement what they actually use. For example, Robots shouldn’t be forced to implement an eat() method.

from abc import ABC, abstractmethod
 
class Worker(ABC):
    @abstractmethod
    def work(self):
        pass
 
    @abstractmethod
    def eat(self):
        pass
 
class Robot(Worker):
    def work(self):
        print("robot working")
 
    def eat(self):
        raise NotImplementedError("robot doesn't eat")
class Workable(ABC):
    @abstractmethod
    def work(self):
        pass
 
class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass
class Human(Workable, Eatable):
    def work(self):
        print("human working")
 
    def eat(self):
        print("human eating")
 
class Robot(Workable):
    def work(self):
        print("robot working")
 

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

class FileLogger:
    def log(self, msg):
        print(f"file log: {msg}")
 
class UserService:
    def __init__(self):
        self.logger = FileLogger()   # tightly coupled
 
    def create_user(self, user):
        self.logger.log("user created")
 
from abc import ABC, abstractmethod
 
class Logger(ABC):
    @abstractmethod
    def log(self, msg):
        pass
 
class FileLogger(Logger):
    def log(self, msg):
        print(f"file log: {msg}")
 
class DBLogger(Logger):
    def log(self, msg):
        print(f"db log: {msg}")
 
class UserService:
    def __init__(self, logger: Logger):   # injected dependency
        self.logger = logger
 
    def create_user(self, user):
        self.logger.log("user created")
 

DIP means high-level modules depend on abstractions, not concrete classes. For example, UserService should accept a Logger interface instead of creating a FileLogger inside.