DEV Community

Cover image for SOLID Principles for Python Developers
Maksym
Maksym

Posted on

SOLID Principles for Python Developers

Introduction

SOLID is an acronym that represents five fundamental design principles that help create maintainable, flexible, and scalable object-oriented software. These principles, introduced by Robert C. Martin (Uncle Bob), are essential for writing clean, robust Python code.

The five SOLID principles are:

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Let's explore each principle with practical Python examples.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one responsibility or job.

❌ Violating SRP

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_to_database(self):
        # Database logic
        print(f"Saving {self.name} to database")

    def send_email(self):
        # Email logic
        print(f"Sending email to {self.email}")

    def validate_email(self):
        # Validation logic
        return "@" in self.email
Enter fullscreen mode Exit fullscreen mode

Problems: The User class handles user data, database operations, email sending, and validation. It has multiple reasons to change.

✅ Following SRP

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user):
        print(f"Saving {user.name} to database")

class EmailService:
    def send_email(self, user):
        print(f"Sending email to {user.email}")

class EmailValidator:
    @staticmethod
    def validate(email):
        return "@" in email

# Usage
user = User("John Doe", "john@example.com")
if EmailValidator.validate(user.email):
    UserRepository().save(user)
    EmailService().send_email(user)
Enter fullscreen mode Exit fullscreen mode

Benefits: Each class now has a single responsibility, making the code more maintainable and testable.


2. Open/Closed Principle (OCP)

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

❌ Violating OCP

class PaymentProcessor:
    def process_payment(self, payment_type, amount):
        if payment_type == "credit_card":
            print(f"Processing ${amount} via Credit Card")
        elif payment_type == "paypal":
            print(f"Processing ${amount} via PayPal")
        elif payment_type == "bitcoin":  # New requirement
            print(f"Processing ${amount} via Bitcoin")
        # Need to modify this method for each new payment type
Enter fullscreen mode Exit fullscreen mode

Problems: Adding new payment methods requires modifying existing code.

✅ Following OCP

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount):
        pass

class CreditCardPayment(PaymentMethod):
    def process(self, amount):
        print(f"Processing ${amount} via Credit Card")

class PayPalPayment(PaymentMethod):
    def process(self, amount):
        print(f"Processing ${amount} via PayPal")

class BitcoinPayment(PaymentMethod):
    def process(self, amount):
        print(f"Processing ${amount} via Bitcoin")

class PaymentProcessor:
    def process_payment(self, payment_method: PaymentMethod, amount):
        payment_method.process(amount)

# Usage
processor = PaymentProcessor()
processor.process_payment(CreditCardPayment(), 100)
processor.process_payment(PayPalPayment(), 50)
processor.process_payment(BitcoinPayment(), 75)
Enter fullscreen mode Exit fullscreen mode

Benefits: New payment methods can be added without modifying existing code.


3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without breaking the application.

❌ Violating LSP

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

class Sparrow(Bird):
    def fly(self):
        print("Sparrow flying")

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly!")

def make_bird_fly(bird: Bird):
    bird.fly()  # This breaks with Penguin

# Usage
sparrow = Sparrow()
penguin = Penguin()

make_bird_fly(sparrow)  # Works fine
make_bird_fly(penguin)  # Throws exception!
Enter fullscreen mode Exit fullscreen mode

Problems: Penguin cannot substitute Bird without breaking functionality.

✅ Following LSP

from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def move(self):
        pass

class FlyingBird(Bird):
    def move(self):
        self.fly()

    def fly(self):
        print("Flying")

class SwimmingBird(Bird):
    def move(self):
        self.swim()

    def swim(self):
        print("Swimming")

class Sparrow(FlyingBird):
    def fly(self):
        print("Sparrow flying")

class Penguin(SwimmingBird):
    def swim(self):
        print("Penguin swimming")

def make_bird_move(bird: Bird):
    bird.move()

# Usage
sparrow = Sparrow()
penguin = Penguin()

make_bird_move(sparrow)  # Sparrow flying
make_bird_move(penguin)  # Penguin swimming
Enter fullscreen mode Exit fullscreen mode

Benefits: Both subclasses can substitute the base class without breaking functionality.


4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they don't use.

❌ Violating ISP

from abc import ABC, abstractmethod

class Worker(ABC):
    @abstractmethod
    def work(self):
        pass

    @abstractmethod
    def eat(self):
        pass

    @abstractmethod
    def sleep(self):
        pass

class Human(Worker):
    def work(self):
        print("Human working")

    def eat(self):
        print("Human eating")

    def sleep(self):
        print("Human sleeping")

class Robot(Worker):
    def work(self):
        print("Robot working")

    def eat(self):
        # Robots don't eat!
        raise NotImplementedError("Robots don't eat")

    def sleep(self):
        # Robots don't sleep!
        raise NotImplementedError("Robots don't sleep")
Enter fullscreen mode Exit fullscreen mode

Problems: Robot is forced to implement methods it doesn't need.

✅ Following ISP

from abc import ABC, abstractmethod

class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self):
        pass

class Human(Workable, Eatable, Sleepable):
    def work(self):
        print("Human working")

    def eat(self):
        print("Human eating")

    def sleep(self):
        print("Human sleeping")

class Robot(Workable):
    def work(self):
        print("Robot working")

# Usage
def manage_worker(worker: Workable):
    worker.work()

def feed_worker(worker: Eatable):
    worker.eat()

human = Human()
robot = Robot()

manage_worker(human)  # Works
manage_worker(robot)  # Works

feed_worker(human)    # Works
# feed_worker(robot)  # Won't compile - Robot doesn't implement Eatable
Enter fullscreen mode Exit fullscreen mode

Benefits: Classes only implement the interfaces they actually need.


5. Dependency Inversion Principle (DIP)

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

❌ Violating DIP

class MySQLDatabase:
    def save(self, data):
        print(f"Saving {data} to MySQL database")

class UserService:
    def __init__(self):
        self.database = MySQLDatabase()  # Direct dependency on concrete class

    def create_user(self, user_data):
        # Some business logic
        self.database.save(user_data)
Enter fullscreen mode Exit fullscreen mode

Problems: UserService is tightly coupled to MySQLDatabase. Changing to PostgreSQL requires modifying UserService.

✅ Following DIP

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def save(self, data):
        pass

class MySQLDatabase(Database):
    def save(self, data):
        print(f"Saving {data} to MySQL database")

class PostgreSQLDatabase(Database):
    def save(self, data):
        print(f"Saving {data} to PostgreSQL database")

class MongoDatabase(Database):
    def save(self, data):
        print(f"Saving {data} to MongoDB")

class UserService:
    def __init__(self, database: Database):
        self.database = database  # Depends on abstraction

    def create_user(self, user_data):
        # Some business logic
        self.database.save(user_data)

# Usage
mysql_db = MySQLDatabase()
postgres_db = PostgreSQLDatabase()
mongo_db = MongoDatabase()

user_service_mysql = UserService(mysql_db)
user_service_postgres = UserService(postgres_db)
user_service_mongo = UserService(mongo_db)

user_service_mysql.create_user("John Doe")
user_service_postgres.create_user("Jane Smith")
user_service_mongo.create_user("Bob Johnson")
Enter fullscreen mode Exit fullscreen mode

Benefits: UserService can work with any database implementation without modification.


Real-World Example: E-commerce Order System

Let's see how all SOLID principles work together in a practical e-commerce order processing system:

from abc import ABC, abstractmethod
from typing import List

# Single Responsibility Principle
class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

class Order:
    def __init__(self):
        self.items: List[Product] = []

    def add_item(self, product: Product):
        self.items.append(product)

    def get_total(self) -> float:
        return sum(item.price for item in self.items)

# Interface Segregation Principle
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass

class NotificationSender(ABC):
    @abstractmethod
    def send_notification(self, message: str) -> None:
        pass

class OrderRepository(ABC):
    @abstractmethod
    def save_order(self, order: Order) -> None:
        pass

# Open/Closed Principle & Liskov Substitution Principle
class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via Credit Card")
        return True

class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via PayPal")
        return True

class EmailNotifier(NotificationSender):
    def send_notification(self, message: str) -> None:
        print(f"Email: {message}")

class SMSNotifier(NotificationSender):
    def send_notification(self, message: str) -> None:
        print(f"SMS: {message}")

class DatabaseOrderRepository(OrderRepository):
    def save_order(self, order: Order) -> None:
        print(f"Saving order with {len(order.items)} items to database")

# Dependency Inversion Principle
class OrderService:
    def __init__(
        self,
        payment_processor: PaymentProcessor,
        notifier: NotificationSender,
        order_repository: OrderRepository
    ):
        self.payment_processor = payment_processor
        self.notifier = notifier
        self.order_repository = order_repository

    def process_order(self, order: Order) -> bool:
        total = order.get_total()

        if self.payment_processor.process_payment(total):
            self.order_repository.save_order(order)
            self.notifier.send_notification(f"Order processed successfully! Total: ${total}")
            return True
        else:
            self.notifier.send_notification("Payment failed!")
            return False

# Usage
def main():
    # Create products
    laptop = Product("Laptop", 999.99)
    mouse = Product("Mouse", 25.99)

    # Create order
    order = Order()
    order.add_item(laptop)
    order.add_item(mouse)

    # Configure services (Dependency Injection)
    payment_processor = CreditCardProcessor()
    notifier = EmailNotifier()
    order_repository = DatabaseOrderRepository()

    # Process order
    order_service = OrderService(payment_processor, notifier, order_repository)
    success = order_service.process_order(order)

    print(f"Order processed: {success}")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Key Benefits of SOLID Principles

  1. Maintainability: Code is easier to understand and modify
  2. Testability: Individual components can be tested in isolation
  3. Flexibility: Easy to extend functionality without breaking existing code
  4. Reusability: Well-designed components can be reused across projects
  5. Reduced Coupling: Components are loosely coupled, making the system more resilient

Conclusion

SOLID principles are not just theoretical concepts—they're practical guidelines that lead to better software design. By applying these principles in your Python projects, you'll create code that is more maintainable, testable, and adaptable to changing requirements.

Remember: Start small, apply these principles gradually, and don't over-engineer. The goal is to write clean, maintainable code that serves your project's needs effectively.

Top comments (0)