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.