The SOLID Principles Explained with Real Examples

SOLID is frequently taught as five abstract rules to memorize — far more useful understood through what actually goes wrong in code that violates each one.

# S - Single Responsibility: a class should have ONE reason to change
class ReportGenerator:       # BAD: formats AND saves AND emails - three reasons to change
    def generate_and_email(self, data): ...

class ReportFormatter:       # GOOD: each class has one job
    def format(self, data): ...
class ReportSender:
    def send(self, report): ...

# O - Open/Closed: extend behavior without modifying existing code
class Discount:
    def apply(self, price): return price
class StudentDiscount(Discount):     # NEW discount type - existing code untouched
    def apply(self, price): return price * 0.9

# L - Liskov Substitution: a subclass must be usable wherever its parent is expected
class Bird:
    def fly(self): ...
class Penguin(Bird):          # BAD: violates LSP - penguins can't actually fly
    def fly(self): raise NotImplementedError()

# D - Dependency Inversion: depend on abstractions, not concrete implementations
class EmailService:
    def __init__(self, sender: 'MessageSender'):  # interface, not a concrete SMTP class
        self.sender = sender

The Interface Segregation Principle (I) — favoring several small, specific interfaces over one large general-purpose one — is the one most commonly skipped, but it’s what prevents a class from being forced to implement methods it genuinely has no use for.