Please use a larger screen

This presentation is designed for desktop or projector displays.

← Back to curriculum

Python Object Oriented Programming

SOLID Principles

Course 7

S O L I D

to navigate

Course Curriculum

Where we are in the journey

Quiz Time! (Lecture 04)

What is the purpose of an abstract class?

A) To create objects directly

B) To define a common interface for subclasses

C) To make all methods private

D) To prevent inheritance

Abstract classes define a contract — subclasses must implement the abstract methods, ensuring a consistent interface

Recap: Lecture 04

Polymorphism: Abstract Classes

  • Abstract classes use ABC + @abstractmethod
  • Cannot be instantiated directly
  • Force subclasses to implement specific methods
  • Key to polymorphism — same interface, different behavior
from abc import ABC
from abc import abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def area(self):
        return 3.14 * self.r**2

Quiz Time! (Lecture 05)

What is the key difference between composition and aggregation?

A) Composition uses inheritance

B) In composition, parts die with the owner

C) Aggregation is stronger than composition

D) There is no difference

Composition = strong ownership (parts created inside, die with owner). Aggregation = weak reference (parts passed in, survive independently).

Recap: Lecture 05

Composition: has-a Relationship

  • Composition: parts created inside the owner, destroyed together
  • Aggregation: parts passed in, can exist independently
  • Prefer composition over inheritance when there's no "is-a" relationship
class Engine:
    def start(self):
        return "Vroom!"

class Car:
    def __init__(self):
        self.engine = Engine()

Quiz Time! (Lecture 06)

When does the finally block execute?

A) Only when an exception occurs

B) Only when no exception occurs

C) Always, regardless of exceptions

D) Only when except is missing

The finally block always executes — even if try has a return statement. Use it for cleanup (closing files, releasing resources).

Recap: Lecture 06

Exception Handling: try/except/finally

  • try — code that might fail
  • except — handle specific errors
  • else — runs if no exception
  • finallyalways runs (cleanup)
  • raise — throw your own exceptions
try:
    file = open("data.csv")
except FileNotFoundError:
    print("File missing!")
else:
    print("File loaded OK")
finally:
    print("Cleanup done")

Agenda for Today

SOLID — deep dive:

Software is not static.
It evolves.

We need code that is easy to:

🔎 Understand

🛠️ Modify

🧩 Extend

Test

Think of it like building with LEGOs

🧱 Functions and Data Structures are like individual LEGO bricks: the basic building blocks of your code.

🏭 Classes are like organized LEGO sets: they group related bricks and give them a specific purpose.

🔨 Imagine you want to add a new room. If you built it following good instructions, you can easily add it without taking the whole house apart.

LEGO bricks

We need instructions how to:

SOLID principles

are about creating code that's not just functional, but also well-structured, maintainable, and reusable.

They are about writing code that is easy to work with in the long run.

SOLID design principles

  • Introduced by Robert C. Martin (Uncle Bob)
  • Frequently referenced in Design Pattern literature
  • Books: Clean Code, Clean Architecture, The Clean Coder
These principles guide how to arrange functions and data structures into classes, and how those classes should be interconnected.
Clean Code by Robert C. Martin

SOLID

S Single Responsibility O Open-Closed Principle L Liskov Substitution I Interface Segregation D Dependency Inversion

Single Responsibility Principle

“There should never be more than one reason
for a class to change.”

or

“A module should be responsible to one,
and only one, actor.”

Overloaded Swiss Army knife

What SRP is NOT?!

“A function should do one, and only one, thing.”

This is a refactoring principle, to refactor large functions into smaller functions; we use it at the lowest levels.

But it's NOT the Single Responsibility Principle.

What SRP IS:

We want to:

Cohesion

How well the internal elements of a module work together to fulfill a single, well-defined purpose.

Coupling

The degree of direct knowledge or reliance one module has on another. Lower is better.

What is the reason to change?

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function.

Ask yourself: who are the actors (stakeholders) that might request changes to this class?

  • 🧑‍💻 The developer (logic changes)
  • 📊 The accountant (reporting changes)
  • 🗃️ The DBA (persistence changes)
If a class serves multiple actors, it has multiple reasons to change — and violates SRP.

Symptoms of SRP violation

Accidental duplication: We put code that different actors depend on into close proximity. SRP says to separate code that different actors depend on.

Merge conflicts: Multiple people changing the same source file for different reasons. Solution — move the functions into different classes.

💡 Gather together the things that change for the same reasons. Separate those things that change for different reasons.

SRP violation example

class UserProfile:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def save(self):
        with open(f"{self.username}_profile.txt", "w") as file:
            file.write(f"Username: {self.username}\n Email: {self.email}")

The UserProfile class violates SRP because it has two reasons to change:

  1. Changes related to how user profiles are managed
  2. Changes related to how user profiles are persisted

✅ Refactored:

class UserProfile:
    def __init__(self, username, email):
        self.username = username
        self.email = email

class UserDataStorage:
    def save_to_file(self, user_profile, filename):
        with open(filename, "w") as file:
            file.write(f"Username: {user_profile.username}\n Email: {user_profile.email}")

SRP Benefits

Open-Closed Principle

“Software entities (classes, modules, functions, etc.)
should be open for extension
but closed for modification.”

Open-Closed Principle

Main idea:

Software must be designed to allow the behavior of it to be
changed by adding new code, rather than changing existing code.

OCP Visualized

❌ Usual way: modify existing Blue = changed — changes scattered everywhere ✅ OCP way: extend only 🧩 Green = new — just add a new piece

Applying OCP

To adhere to OCP, software developers often employ strategies such as:

OCP example

A system that processes documents. Initially, it only handles Word:

class DocumentProcessor:
    def process(self, doc):
        if doc.type == "Word":
            self.process_word(doc)

    def process_word(self, doc):
        pass

✅ According to OCP, if we want to add PDF support, we shouldn't alter the existing code but extend it:

class PDFProcessor(DocumentProcessor):
    def process(self, doc):
        if doc.type == "PDF":
            self.process_pdf(doc)
        else:
            super().process(doc)

    def process_pdf(self, doc):
        pass

OCP Benefits

Liskov Substitution Principle

“Subclasses should be substitutable
for their base classes.”

or

“You should be able to use the child class
anywhere you'd use the parent class,
without breaking your code.”

Square peg, round hole

Liskov Substitution Principle (LSP)

Substitutable = capable of being exchanged.

Users of a base class should continue to function properly if a derivative of that base class is passed to it.

💡 If you have a function that works with Animal, it should also work with Dog (which is a type of Animal) without needing any special adjustments.

Named after Barbara Liskov, who introduced it in 1987.

LSP violation: Bird & Penguin

class Bird:
    def fly(self):
        return "Flying high!"

    def eat(self):
        return "Eating..."


class Penguin(Bird):
    def fly(self):
        raise Exception("I can't fly!")

A Penguin is-a Bird, right?

But any code that expects a Bird to fly() will crash when given a Penguin.

The subclass breaks the contract of its parent.

def make_bird_fly(bird):
    print(bird.fly())

make_bird_fly(Bird())     # "Flying high!"
make_bird_fly(Penguin())  # Exception! LSP violation

LSP fix: proper hierarchy

from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def eat(self):
        pass

class FlyingBird(Bird):
    def fly(self):
        return "Flying high!"
    def eat(self):
        return "Eating seeds"

class SwimmingBird(Bird):
    def swim(self):
        return "Swimming!"
    def eat(self):
        return "Eating fish"
class Eagle(FlyingBird):
    pass

class Penguin(SwimmingBird):
    pass

class Duck(FlyingBird, SwimmingBird):
    pass

Now Penguin never promises to fly. Only FlyingBird subclasses have fly().

Every subclass honors its parent's contract.

Violating LSP consequences

Implications of LSP:

Interface Segregation Principle

“Many client specific interfaces are better
than one general purpose interface.”

Separate cables vs one multi-adapter

Interface Segregation Principle

Ensure that your classes only implement the methods they need. If a class is forced to implement methods it doesn't use, consider breaking your interface (or ABC) into smaller, more specific ones.

💡 Think of it like USB cables: individual cables for each device (✅) vs one cable with every connector (❌).

ISP violation example

❌ One fat interface forces all classes to implement everything:

from abc import ABC, abstractmethod

class Machine(ABC):
    @abstractmethod
    def print(self, doc): pass
    @abstractmethod
    def scan(self, doc): pass
    @abstractmethod
    def fax(self, doc): pass

A simple printer is forced to implement scan() and fax() even though it can't do either:

class SimplePrinter(Machine):
    def print(self, doc):
        print(f"Printing {doc}")

    def scan(self, doc):
        raise NotImplementedError("Can't scan!")

    def fax(self, doc):
        raise NotImplementedError("Can't fax!")
💡 This also violates LSP — a SimplePrinter can't be used wherever a Machine is expected. The principles reinforce each other!

ISP ✅ fix: Printer / Scanner / Fax

abstract Printer abstract Scanner abstract Fax concrete SimplePrinter implements: Printer concrete Photocopier implements: Printer, Scanner concrete MultiFunctionPrinter implements: Printer, Scanner, Fax

ISP Code Example

Abstract interfaces (ABCs):

class Printer:
    @abstractmethod
    def print(self, document):
        pass

class Scanner:
    @abstractmethod
    def scan(self, document):
        pass

class Fax:
    @abstractmethod
    def fax(self, document):
        pass

Concrete implementations:

class SimplePrinter(Printer):
    def print(self, document):
        ...

class Photocopier(Printer, Scanner):
    def print(self, document):
        ...
    def scan(self, document):
        ...

class MultiFunctionPrinter(Printer, Scanner, Fax):
    def print(self, document):
        ...
    def scan(self, document):
        ...
    def fax(self, document):
        ...

Implications of ISP

Dependency Inversion Principle

“Depend upon Abstractions.
Do not depend upon concretions.”

Warehouse with delivery routes

DIP Solution

One should depend upon interfaces or abstract functions & classes, rather than upon concrete functions & classes.

DIP Visualized

❌ Without DIP High-Level Module Low-Level Module ✅ With DIP High-Level Module Abstraction Layer Low-Level Module

DIP violation: SellerWarehouse

❌ Violation:

class SellerWarehouse:
    def airplane_delivery(self):
        print(f"Airplane to {address}")
    def truck_delivery(self):
        print(f"Truck to {address}")
    def ship_delivery(self):
        print(f"Ship to {address}")

warehouse = SellerWarehouse()
if type == "airplane":
    warehouse.airplane_delivery()
if type == "truck":
    warehouse.truck_delivery()
if type == "ship":
    warehouse.ship_delivery()

✅ Fixed with dependency injection:

from abc import ABC, abstractmethod

class Delivery(ABC):
    def __init__(self, address):
        self.address = address
    @abstractmethod
    def deliver(self): pass

class AirplaneDelivery(Delivery):
    def deliver(self):
        print(f"Airplane to {self.address}")

class SellerWarehouse:
    def __init__(self, delivery):
        self.delivery = delivery
    def deliver(self):
        self.delivery.deliver()

DIP Extensibility

Now adding a new delivery method is trivial — no existing code needs to change:

class DroneDelivery(Delivery):
    def deliver(self):
        print(f"Drone delivery to {self.address}")

address = "Pilies 22, Vilnius, Lithuania"
delivery = DroneDelivery(address)
warehouse = SellerWarehouse(delivery)
warehouse.deliver()
💡 Remember dependency injection from Lecture 05 (Composition)? DIP is the principle, dependency injection is the technique. DIP also connects to OCP (extend without modifying) and LSP (any Delivery subclass works).

🔍 Spot the violation!

class Order:
    def __init__(self, items):
        self.items = items

    def calculate_total(self):
        return sum(item.price for item in self.items)

    def generate_invoice(self):
        return f"Invoice: {self.calculate_total()}"

    def save_to_database(self):
        db.save(self)

A) SRP — three reasons to change

B) LSP — can't substitute subclass

C) DIP — depends on concretions

D) No violation

Order has 3 responsibilities: business logic, presentation (invoice), and persistence (database). Each could change for a different reason.

✅ Fix: separate responsibilities

class Order:
    def __init__(self, items):
        self.items = items

    def calculate_total(self):
        return sum(
            item.price for item in self.items
        )

class InvoiceGenerator:
    def generate(self, order):
        return f"Invoice: {order.calculate_total()}"
class OrderRepository:
    def save(self, order):
        db.save(order)
  • Order — only business logic
  • InvoiceGenerator — only presentation
  • OrderRepository — only persistence

🔍 Spot the violation!

class NotificationService:
    def send(self, message, channel):
        if channel == "email":
            self._send_email(message)
        elif channel == "sms":
            self._send_sms(message)
        elif channel == "push":
            self._send_push(message)

A) ISP — fat interface

B) OCP — must modify to add channels

C) LSP — broken substitution

D) SRP — multiple responsibilities

Adding Slack or WhatsApp means editing this class. OCP says extend with new classes, don't modify existing ones.

✅ Fix: open for extension

from abc import ABC, abstractmethod

class NotificationChannel(ABC):
    @abstractmethod
    def send(self, message): pass

class EmailChannel(NotificationChannel):
    def send(self, message):
        print(f"Email: {message}")

class SMSChannel(NotificationChannel):
    def send(self, message):
        print(f"SMS: {message}")
class SlackChannel(NotificationChannel):
    def send(self, message):
        print(f"Slack: {message}")

class NotificationService:
    def send(self, message, channel):
        channel.send(message)

Adding Slack? Just create a new class. Zero changes to existing code.

This also applies DIPNotificationService depends on the abstraction, not concrete channels.

🔍 Spot the violation(s)!

class Vehicle:
    def start_engine(self):
        return "Engine started"

    def accelerate(self):
        return "Accelerating"

class Bicycle(Vehicle):
    def start_engine(self):
        raise Exception("Bicycles don't have engines!")

    def accelerate(self):
        return "Pedaling faster"

A) Only OCP

B) Only SRP

C) LSP + ISP

D) DIP only

LSP: Bicycle breaks the Vehicle contract (can't start engine). ISP: Vehicle forces an engine interface on vehicles that don't have one. Same pattern as Bird/Penguin!

✅ Fix: proper hierarchy

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def accelerate(self): pass

class MotorizedVehicle(Vehicle):
    def start_engine(self):
        return "Engine started"

class HumanPoweredVehicle(Vehicle):
    def pedal(self):
        return "Pedaling"
class Car(MotorizedVehicle):
    def accelerate(self):
        return "Car accelerating"

class Bicycle(HumanPoweredVehicle):
    def accelerate(self):
        return "Pedaling faster"
  • 🟢 LSP: Bicycle never promises an engine
  • 🟢 ISP: Engine interface only on motorized vehicles
  • Same fix pattern as Bird/Penguin!

🔍 How many violations?

class ReportGenerator:
    def __init__(self):
        self.db = MySQLDatabase()

    def fetch_data(self):
        return self.db.query("SELECT * FROM reports")

    def generate_pdf(self, data):
        print(f"PDF with {len(data)} rows")

    def send_email(self, pdf):
        print(f"Emailing {pdf}")

A) 1 (SRP only)

B) 2 (SRP + DIP)

C) 3 (SRP + OCP + DIP)

D) All 5 principles

SRP: fetching, generating, and emailing are 3 responsibilities. DIP: hardcoded MySQLDatabase() instead of injecting an abstraction.

✅ Fix: separate + inject

class ReportRepository:
    def __init__(self, db):
        self.db = db

    def fetch(self):
        return self.db.query(
            "SELECT * FROM reports"
        )

class PDFGenerator:
    def generate(self, data):
        print(f"PDF: {len(data)} rows")

class EmailService:
    def send(self, attachment):
        print(f"Emailing {attachment}")
# Usage with dependency injection:
db = MySQLDatabase()
repo = ReportRepository(db)
pdf = PDFGenerator()
email = EmailService()

data = repo.fetch()
report = pdf.generate(data)
email.send(report)
  • 🔳 SRP: each class has one job
  • 🟢 DIP: database is injected, not hardcoded
  • 🔸 OCP: swap PDF for Excel? New class, zero changes

When NOT to apply SOLID

SOLID is a guideline, not a law. Don't over-engineer:

Start simple. Refactor toward SOLID when the pain appears — when a class gets too big, when adding features requires editing 5 files, when tests become impossible.

Common pitfalls

Key Takeaways

SOLID principles work together. They make your code easier to understand, modify, extend, and test. Start applying them gradually — you don't need to be perfect on day one.

Next up: UML & Design Patterns