šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

SOLID Principles in Python

The five SOLID principles, translated from their Java/C# origins into idiomatic, Pythonic examples that actually reflect how experienced Python engineers design systems.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What does "closed for modification" mean in the Open/Closed Principle?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

SOLID was coined for statically-typed, class-heavy languages, and a literal port into Python often produces over-engineered code. This lesson covers what each principle actually protects against, and how to apply that intent — not the letter — in a language that also has functions, protocols, and duck typing as first-class design tools.

1SRP and OCP: Designing for the Next Change, Not Just Today

The Single Responsibility Principle is often summarized as 'a class should have one reason to change', which is more useful read backwards: ask what would force you to edit this class, and if the answer includes two unrelated things — 'the report's formatting changes' *and* 'we switch email providers' — that class is doing two jobs that should be able to change independently. Splitting ReportGenerator and EmailSender apart means a change to email delivery logic can never accidentally introduce a bug in report formatting, and vice versa; each class can be tested, reasoned about, and modified in isolation.

The Open/Closed Principle extends the same instinct to how a system grows over time: code should be extensible without requiring edits to existing, already-tested logic. An if/elif chain dispatching on a discount_type string violates this directly — every new discount type means editing a function that every existing discount type also runs through, with real risk of breaking one type while adding another. Replacing the chain with a Discount Protocol (or an abstract base class) and one small class per discount type means adding SeasonalDiscount is purely additive: write a new class, register it, and every existing discount type's code path is never touched.

Both principles point at the same underlying value: isolating the blast radius of a change. SRP isolates it by responsibility (this class only breaks for this reason); OCP isolates it by extension point (adding new behavior doesn't risk existing behavior).

āœ•
—
+
# Violates SRP: formatting AND delivery in one class
class ReportGenerator:
    def generate(self, data): ...
    def send_email(self, report, address): ...

# Follows SRP: each class has one job
class ReportGenerator:
    def generate(self, data): ...

class EmailSender:
    def send(self, content, address): ...
localhost:3000
Design Impact
Adding a new discount type
One new class — zero lines of existing code touched

2LSP: Subclassing Is a Behavioral Contract, Not Just a Type Relationship

The Liskov Substitution Principle states that if Square inherits from Rectangle, then any code written to work correctly with a Rectangle must continue to work correctly when given a Square instead — substitutability, not just 'is-a' in a geometric or intuitive sense. The classic counterexample is exactly this pair: a Rectangle's implicit contract is that .width and .height vary independently; a Square that forces them to stay equal (changing one when the other is set) breaks that contract for any caller that relied on it, even though 'a square is a rectangle' is true mathematically.

This is why LSP is fundamentally about *behavior*, not type hierarchies in the abstract — a violation doesn't show up as a type error, it shows up as a subtle correctness bug in code that assumed the parent class's guarantees still held. The practical warning sign is a subclass that overrides a method to do noticeably less, throw where the parent wouldn't, or introduce a side effect the parent's contract never promised (or explicitly promised *wouldn't* happen).

The professional habit LSP encourages is thinking about a base class (or Protocol) as a *contract*, not just a set of method names to implement — documenting what each method promises (and doesn't), so that anyone writing a subclass knows exactly what substitutability requires, and reviewers have a concrete standard to check new subclasses against.

āœ•
—
+
# Violates OCP: adding a discount type means editing this function
def apply_discount(order, discount_type):
    if discount_type == "percent":
        ...
    elif discount_type == "fixed":
        ...
    # every new type = another elif here

# Follows OCP: new discount types are added WITHOUT touching existing code
class Discount(Protocol):
    def apply(self, total: float) -> float: ...

class PercentDiscount:
    def apply(self, total): return total * 0.9
localhost:3000
Contract Violation
Square coupling width/height
Breaks Rectangle's implicit "independent dimensions" contract

3ISP and DIP: Small Interfaces, and Depending on Abstractions

The Interface Segregation Principle argues against one large, do-everything interface in favor of several small, focused ones: a class implementing Printable (just to_pdf()) shouldn't be forced to also implement unrelated methods from a bloated Document interface it doesn't actually need. In Python, this maps directly onto the Protocol construct from the Type Hints lesson — small, structural interfaces that a class satisfies simply by having the right methods, with no forced inheritance from a monolithic base class that drags in unrelated obligations.

The Dependency Inversion Principle says high-level code should depend on abstractions (interfaces/Protocols), not on concrete, low-level implementations directly — def export(report: Printable) -> bytes: depends on the shape 'has a to_pdf() method', not on any specific report class. This is what makes the function usable with a PDFReport, a HTMLReport wrapped in an adapter, or a test double that fakes to_pdf(), all without the function itself ever changing.

Together, ISP and DIP are why professional Python code so often accepts a Protocol-typed parameter instead of a concrete class: it keeps the dependency direction pointing at a small, stable abstraction rather than a large, concrete, frequently-changing implementation — exactly the property that makes a codebase's pieces replaceable and testable in isolation, which is the practical payoff every SOLID principle is ultimately in service of.

āœ•
—
+
class Rectangle:
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h

class Square(Rectangle):
    def __init__(self, side): super().__init__(side, side)
    # If setting .w also silently forces .h to match, callers
    # expecting independent w/h (like Rectangle's contract) get surprised
localhost:3000
Dependency Direction
export(report: Printable)
Depends on a small abstraction, not a concrete report class

4Step-by-Step Breakdown

SOLID isn't five arbitrary rules — it's five answers to 'what tends to make code hard to change six months from now?' Let's see each one through a Python lens.

Single Responsibility: a class should have one reason to change. A ReportGenerator that also handles email sending has two.

Open/Closed: code should be open for extension, closed for modification. A discount system that needs an if/elif edited for every new type violates this.

Checkpoint: What does "closed for modification" mean in the Open/Closed Principle?

  • →Adding new behavior should not require editing existing, already-tested code
  • →All methods on the class must be private

Liskov Substitution: a subclass must be usable anywhere its parent is expected, without breaking correctness.

Checkpoint: What does the Square/Rectangle example illustrate about Liskov Substitution?

  • →A subclass that changes the parent's expected behavior can silently break code written against the parent
  • →That squares and rectangles should never be related by inheritance in any language

Interface Segregation and Dependency Inversion: depend on small, focused interfaces, and depend on abstractions rather than concrete implementations.

SOLID gives you principles; composition vs. inheritance is the concrete decision you'll apply them through most often.

Apply a Real Open/Closed Discount. Finish checkout_total(): adding a discount type never touches this function.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Apply SOLID for the problem it solves, not as a checklist to satisfy on every class

A three-method utility class doesn't need an interface and a factory to be 'SOLID compliant' — over-applying these principles to simple, stable code adds indirection with no real payoff. Reach for them when a class's responsibilities or extension points are genuinely unclear or growing.

Use Protocol for ISP/DIP in Python instead of forcing ABC inheritance everywhere

Python's structural typing means you often don't need a formal interface hierarchy at all — a Protocol describing exactly the methods a function needs achieves the same dependency-inversion benefit with less ceremony than Java/C#-style interfaces.

Frequent Bugs

THE BUG

Subclassing purely for code reuse ("this new class needs 80% of what Rectangle already does") without verifying the subclass actually honors the parent's full behavioral contract, silently violating LSP.

THE FIX

Before subclassing for reuse, check whether every method the parent class promises still behaves correctly (not just compiles) on the subclass. If the contract doesn't hold, prefer composition (the next lesson) over inheritance.

Real-World Examples

Refactoring a Discount if/elif Chain to Satisfy OCP

An e-commerce checkout function's discount logic has grown to a 15-branch if/elif chain, and every new promotion type requires editing that same function, with real risk of introducing a regression in an unrelated discount type.

from typing import Protocol

class Discount(Protocol):
    def apply(self, total: float) -> float: ...

class PercentDiscount:
    def __init__(self, percent: float):
        self.percent = percent
    def apply(self, total: float) -> float:
        return total * (1 - self.percent / 100)

class FixedDiscount:
    def __init__(self, amount: float):
        self.amount = amount
    def apply(self, total: float) -> float:
        return max(0, total - self.amount)

def checkout_total(total: float, discount: Discount) -> float:
    return discount.apply(total)

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Adding a new type of behavior (e.g. a new discount type) by editing an existing if/elif chain that every other type also depends on, risking a regression in unrelated, already-working code paths.

# Violates OCP: editing shared logic for every new case def apply_discount(total, kind): if kind == "percent": ... elif kind == "fixed": ... elif kind == "seasonal": # new: risks breaking the above ... # Follows OCP: additive, not modificative class SeasonalDiscount: def apply(self, total: float) -> float: ...

The Solution //

Refactor to a Protocol (or ABC) with one small class per behavior variant, so new behavior is added purely by writing a new class — no existing, tested code path is ever modified.

Lesson Glossary

[01]SRP (Single Responsibility Principle)

The principle that a class should have exactly one reason to change, i.e. one clear responsibility.

Code Preview
// SRP (Single Responsibility Principle) context

[02]OCP (Open/Closed Principle)

The principle that code should be extensible with new behavior without requiring modification of existing, tested code.

Code Preview
// OCP (Open/Closed Principle) context

[03]LSP (Liskov Substitution Principle)

The principle that a subclass must be substitutable for its parent class without breaking the correctness of code written against the parent.

Code Preview
// LSP (Liskov Substitution Principle) context

[04]DIP (Dependency Inversion Principle)

The principle that code should depend on abstractions (interfaces/Protocols) rather than concrete, low-level implementations.

Code Preview
// DIP (Dependency Inversion Principle) context

Continue Learning