- Abstract classes use
ABC+@abstractmethod - Cannot be instantiated directly
- Force subclasses to implement specific methods
- Key to polymorphism — same interface, different behavior
This presentation is designed for desktop or projector displays.
← Back to curriculumPython Object Oriented Programming
Course 7
← → to navigate
Where we are in the journey
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
ABC + @abstractmethodfrom 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
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
class Engine: def start(self): return "Vroom!" class Car: def __init__(self): self.engine = Engine()
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
try — code that might failexcept — handle specific errorselse — runs if no exceptionfinally — always runs (cleanup)raise — throw your own exceptionstry: file = open("data.csv") except FileNotFoundError: print("File missing!") else: print("File loaded OK") finally: print("Cleanup done")
SOLID — deep dive:
Software is not static.
It evolves.
We need code that is easy to:
🔎 Understand
🛠️ Modify
🧩 Extend
✅ Test
🧱 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.
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.
“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.”
“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.
We want to:
How well the internal elements of a module work together to fulfill a single, well-defined purpose.
The degree of direct knowledge or reliance one module has on another. Lower is better.
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?
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.
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:
✅ 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}")
“Software entities (classes, modules, functions, etc.)
should be open for extension
but closed for modification.”
Main idea:
Software must be designed to allow the behavior of it to be
changed by adding new code, rather than changing existing code.
To adhere to OCP, software developers often employ strategies such as:
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
“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.”
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.
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
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.
Implications of LSP:
“Many client specific interfaces are better
than one general purpose interface.”
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.
❌ 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!")
SimplePrinter can't be used wherever a Machine is expected. The principles reinforce each other!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): ...
“Depend upon Abstractions.
Do not depend upon concretions.”
One should depend upon interfaces or abstract functions & classes, rather than upon concrete functions & classes.
❌ 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()
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()
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.
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 logicInvoiceGenerator — only presentationOrderRepository — only persistenceclass 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.
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 DIP — NotificationService depends on the abstraction, not concrete channels.
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!
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"
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.
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)
SOLID is a guideline, not a law. Don't over-engineer:
math.sqrt)Next up: UML & Design Patterns