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)