SOLID Principles & Clean Code
📖 Concept
The SOLID principles are five design guidelines that help developers write maintainable, extensible, and testable object-oriented code. While originally formulated for statically-typed languages, they apply equally well to Python — with some Pythonic adaptations.
The SOLID principles:
| Principle | Name | Core Idea |
|---|---|---|
| S | Single Responsibility | A class should have only one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for their base types |
| I | Interface Segregation | Many specific interfaces beat one general-purpose interface |
| D | Dependency Inversion | Depend on abstractions, not concrete implementations |
Single Responsibility Principle (SRP): Each class or function should do exactly one thing. If a class handles user authentication and sends emails, split it. In Python, functions are often the right unit of responsibility — not everything needs to be a class.
Open/Closed Principle (OCP): Extend behavior without modifying existing code. In Python, this means using composition over inheritance, plugin registries, or strategy functions. The @functools.singledispatch decorator is a built-in example — add new type handlers without touching the original function.
Liskov Substitution Principle (LSP): A subclass must honor the contract of its parent. If Rectangle has a set_width() method, a Square subclass shouldn't break callers that expect width and height to be independent. In Python, use Protocol classes or ABCs to define explicit contracts.
Interface Segregation Principle (ISP): Don't force classes to implement methods they don't need. Python uses duck typing and Protocol classes (PEP 544) instead of Java-style interfaces — define small, focused protocols.
Dependency Inversion Principle (DIP): High-level modules shouldn't depend on low-level modules; both should depend on abstractions. In Python, pass dependencies as constructor arguments or use Protocol types for type hints.
Clean code practices: Use descriptive names, keep functions short (under 20 lines ideally), avoid deep nesting (use early returns), write docstrings, and prefer composition over inheritance — Python's mixin and protocol patterns make this natural.
💻 Code Example
1# ============================================================2# SOLID Principles in Python — Practical Examples3# ============================================================45from abc import ABC, abstractmethod6from typing import Protocol, runtime_checkable7from dataclasses import dataclass8910# --- S: Single Responsibility Principle ---1112# BAD: One class doing everything13class UserManagerBad:14 def create_user(self, name, email):15 # Validates, saves to DB, sends email — too many responsibilities16 pass1718# GOOD: Separate responsibilities19@dataclass20class User:21 name: str22 email: str2324class UserValidator:25 def validate(self, user: User) -> bool:26 if not user.name or len(user.name) < 2:27 raise ValueError("Name must be at least 2 characters")28 if "@" not in user.email:29 raise ValueError("Invalid email address")30 return True3132class UserRepository:33 def __init__(self):34 self._users: dict[str, User] = {}3536 def save(self, user: User) -> None:37 self._users[user.email] = user3839 def find_by_email(self, email: str) -> User | None:40 return self._users.get(email)4142class EmailService:43 def send_welcome(self, user: User) -> None:44 print(f"Welcome email sent to {user.email}")454647# --- O: Open/Closed Principle ---4849# Using functools.singledispatch for extensibility50from functools import singledispatch5152@dataclass53class TextReport:54 content: str5556@dataclass57class HTMLReport:58 html: str5960@dataclass61class PDFReport:62 binary_data: bytes6364@singledispatch65def export_report(report) -> str:66 raise NotImplementedError(f"No exporter for {type(report)}")6768@export_report.register(TextReport)69def _(report: TextReport) -> str:70 return report.content7172@export_report.register(HTMLReport)73def _(report: HTMLReport) -> str:74 return f"<html><body>{report.html}</body></html>"7576# Extend without modifying existing code — just add new registrations77@export_report.register(PDFReport)78def _(report: PDFReport) -> str:79 return f"PDF ({len(report.binary_data)} bytes)"808182# --- L: Liskov Substitution Principle ---8384class Shape(ABC):85 @abstractmethod86 def area(self) -> float: ...8788class Rectangle(Shape):89 def __init__(self, width: float, height: float):90 self._width = width91 self._height = height9293 def area(self) -> float:94 return self._width * self._height9596class Circle(Shape):97 def __init__(self, radius: float):98 self._radius = radius99100 def area(self) -> float:101 import math102 return math.pi * self._radius ** 2103104# Any Shape can be used interchangeably105def print_total_area(shapes: list[Shape]) -> None:106 total = sum(s.area() for s in shapes)107 print(f"Total area: {total:.2f}")108109print_total_area([Rectangle(3, 4), Circle(5), Rectangle(2, 6)])110111112# --- I: Interface Segregation with Protocols ---113114@runtime_checkable115class Readable(Protocol):116 def read(self) -> str: ...117118@runtime_checkable119class Writable(Protocol):120 def write(self, data: str) -> None: ...121122class FileStorage:123 """Implements both Readable and Writable."""124 def __init__(self):125 self._data = ""126127 def read(self) -> str:128 return self._data129130 def write(self, data: str) -> None:131 self._data = data132133class ReadOnlyCache:134 """Only implements Readable — not forced to implement write."""135 def __init__(self, data: str):136 self._data = data137138 def read(self) -> str:139 return self._data140141def display(source: Readable) -> None:142 """Depends only on what it needs — Readable."""143 print(f"Content: {source.read()}")144145display(FileStorage()) # Works146display(ReadOnlyCache("hi")) # Works — no unused write() method147148149# --- D: Dependency Inversion Principle ---150151class NotificationSender(Protocol):152 def send(self, to: str, message: str) -> None: ...153154class EmailSender:155 def send(self, to: str, message: str) -> None:156 print(f"Email to {to}: {message}")157158class SMSSender:159 def send(self, to: str, message: str) -> None:160 print(f"SMS to {to}: {message}")161162class UserService:163 """Depends on abstraction (Protocol), not concrete sender."""164 def __init__(self, notifier: NotificationSender):165 self._notifier = notifier166167 def register(self, user: User) -> None:168 # Business logic...169 self._notifier.send(user.email, f"Welcome, {user.name}!")170171# Easy to swap implementations and mock in tests172user_svc = UserService(notifier=EmailSender())173user_svc.register(User("Alice", "alice@example.com"))174175user_svc_sms = UserService(notifier=SMSSender())176user_svc_sms.register(User("Bob", "555-0123"))
🏋️ Practice Exercise
Exercises:
Take this monolithic class and refactor it to follow SRP: a
ReportManagerclass that fetches data from a database, applies business rules, formats the output as HTML, and sends it via email. Split it into at least 4 classes.Implement an OCP-compliant payment processing system. Start with
CreditCardPaymentandPayPalPayment, then addCryptoPaymentwithout modifying existing code. Use a registry pattern orsingledispatch.Demonstrate a Liskov Substitution violation: create a
Birdbase class with afly()method, then aPenguinsubclass. Show why this violates LSP and refactor using Protocols to fix it.Refactor a "fat interface" into segregated protocols. Start with a single
IDatabaseclass that hasread(),write(),delete(),backup(), andreplicate()methods. Split into focused protocols and show a class that only implements the ones it needs.Apply Dependency Inversion to a logging system. Create a
LogSinkprotocol with implementations for console, file, and remote HTTP logging. Build anApplicationclass that accepts anyLogSinkvia constructor injection.Review one of your past Python projects and identify at least 3 SOLID violations. Document each violation and propose a refactored solution.
⚠️ Common Mistakes
Creating classes for everything — in Python, functions and modules can fulfill SRP without unnecessary class wrappers. Not everything needs to be a class.
Violating Liskov Substitution by having subclasses raise NotImplementedError for inherited methods. If a subclass can't support a parent's interface, the hierarchy is wrong.
Confusing Dependency Inversion with Dependency Injection. DIP is the principle (depend on abstractions); DI is the technique (pass dependencies in). You can practice DI without following DIP.
Over-segregating interfaces — creating one Protocol per method leads to fragmented code that's harder to understand. Group related behaviors logically.
Applying SOLID rigidly to small scripts or prototypes. These principles shine in large codebases; for a 50-line script, pragmatism beats purity.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for SOLID Principles & Clean Code