AiTechWorlds
AiTechWorlds
University learning companion
Object-Oriented Programming (OOP) · Final Project: Build an OOP-Based Library System
Use this course-level map to place “Final Project: Build an OOP-Based Library System” in the larger system. It complements—not replaces—a topic-specific worked diagram.
A fully functional Library Management System that ties together every OOP concept from this course: abstract classes, inheritance, composition, magic methods, properties, design patterns, and SOLID principles.
By the end of this lesson, you will have a working system that tracks books, manages members, handles borrowing and returns, calculates fines, and sends overdue notifications all built with clean, principled OOP design.
| Feature | Description |
|---|---|
| Items | Books, Magazines, DVDs each with unique attributes |
| Members | Regular and Premium members with different borrowing limits |
| Borrowing | Check out items, enforce limits, track due dates |
| Returns | Return items, calculate fines for late returns |
| Notifications | Alert members and staff when items are overdue |
| Reporting | Search catalog, view borrowing history |
from abc import ABC, abstractmethod
from datetime import date, timedelta
from typing import Optional
class LibraryItem(ABC):
"""Abstract base: every item in the library must implement these."""
def __init__(self, item_id: str, title: str, year: int):
self._item_id = item_id # Unique catalog identifier
self._title = title # Display title
self._year = year # Publication/release year
self._available = True # True = on shelf, False = checked out
@property
def item_id(self) -> str:
return self._item_id
@property
def title(self) -> str:
return self._title
@property
def available(self) -> bool:
return self._available
@abstractmethod
def item_type(self) -> str:
"""Every subclass must declare its type."""
pass
@abstractmethod
def borrow_duration_days(self) -> int:
"""How many days this item type can be borrowed."""
pass
def checkout(self):
if not self._available:
raise ValueError(f"'{self._title}' is not available")
self._available = False # Mark as checked out
def return_item(self):
self._available = True # Mark as returned to shelf
def __repr__(self) -> str:
status = "Available" if self._available else "Checked Out"
return f"{self.item_type()} [{self._item_id}]: '{self._title}' ({self._year}) — {status}"class Book(LibraryItem):
def __init__(self, item_id: str, title: str, year: int, author: str, isbn: str):
super().__init__(item_id, title, year)
self.author = author # Author's full name
self.isbn = isbn # International Standard Book Number
def item_type(self) -> str:
return "Book"
def borrow_duration_days(self) -> int:
return 21 # Books: 3-week loan period
class Magazine(LibraryItem):
def __init__(self, item_id: str, title: str, year: int, issue: str):
super().__init__(item_id, title, year)
self.issue = issue # e.g. "Vol. 12, No. 4"
def item_type(self) -> str:
return "Magazine"
def borrow_duration_days(self) -> int:
return 7 # Magazines: 1-week loan (high demand)
class DVD(LibraryItem):
def __init__(self, item_id: str, title: str, year: int, director: str, runtime_mins: int):
super().__init__(item_id, title, year)
self.director = director # Film director's name
self.runtime_mins = runtime_mins # Film length in minutes
def item_type(self) -> str:
return "DVD"
def borrow_duration_days(self) -> int:
return 5 # DVDs: 5-day loan periodclass BorrowingRecord:
"""Represents one borrowing transaction."""
FINE_PER_DAY = 0.25 # $0.25 fine per overdue day
def __init__(self, item: LibraryItem, member_id: str):
self.item = item # The borrowed item
self.member_id = member_id # Who borrowed it
self.borrow_date = date.today() # When it was checked out
self.due_date = date.today() + timedelta(days=item.borrow_duration_days())
self.return_date: Optional[date] = None # Set when returned
@property
def is_overdue(self) -> bool:
if self.return_date:
return self.return_date > self.due_date # Returned late
return date.today() > self.due_date # Still out, past due
@property
def fine_amount(self) -> float:
if not self.is_overdue:
return 0.0
if self.return_date:
overdue_days = (self.return_date - self.due_date).days
else:
overdue_days = (date.today() - self.due_date).days
return overdue_days * self.FINE_PER_DAY # Accumulate daily fine
def complete_return(self):
self.return_date = date.today() # Mark as returned today
self.item.return_item() # Make item available again
class LibraryMember:
"""Regular library member."""
MAX_ITEMS = 3 # Regular members: borrow up to 3 items
def __init__(self, member_id: str, name: str, email: str):
self.member_id = member_id # Unique membership number
self.name = name # Member's full name
self.email = email # Contact email
self._active_borrows: list[BorrowingRecord] = [] # Current checkouts
self._history: list[BorrowingRecord] = [] # All past records
@property
def active_borrows(self) -> list:
return self._active_borrows
@property
def can_borrow(self) -> bool:
return len(self._active_borrows) < self.MAX_ITEMS # Under the limit
@property
def total_fines(self) -> float:
# Sum fines from active overdue borrows only (completed fines paid at return)
return sum(r.fine_amount for r in self._active_borrows)
def add_borrow(self, record: BorrowingRecord):
self._active_borrows.append(record) # Track new checkout
def complete_return(self, item_id: str) -> BorrowingRecord:
# Find the active record for this item
for record in self._active_borrows:
if record.item.item_id == item_id:
record.complete_return() # Mark returned
self._active_borrows.remove(record) # Remove from active
self._history.append(record) # Move to history
return record
raise ValueError(f"No active borrow found for item {item_id}")
def __repr__(self) -> str:
return f"Member [{self.member_id}]: {self.name} ({len(self._active_borrows)} items borrowed)"
class PremiumMember(LibraryMember):
"""Premium members get extended borrowing privileges."""
MAX_ITEMS = 10 # 10-item limit instead of 3
def __init__(self, member_id: str, name: str, email: str):
super().__init__(member_id, name, email)
self.tier = "Premium" # Membership tier label
@property
def fine_discount(self) -> float:
return 0.5 # Premium members pay 50% of fines
@property
def total_fines(self) -> float:
# Override: apply discount to all fines
base_fines = super().total_fines
return base_fines * (1 - self.fine_discount)class OverdueObserver(ABC):
@abstractmethod
def on_overdue(self, member: LibraryMember, record: BorrowingRecord):
pass
class EmailOverdueNotifier(OverdueObserver):
def on_overdue(self, member: LibraryMember, record: BorrowingRecord):
fine = record.fine_amount
print(f"[EMAIL] To: {member.email} | '{record.item.title}' is overdue. Fine: ${fine:.2f}")
class StaffAlertNotifier(OverdueObserver):
def on_overdue(self, member: LibraryMember, record: BorrowingRecord):
print(f"[STAFF ALERT] {member.name} has overdue item: '{record.item.title}'")class Library:
"""Central system managing the entire library."""
def __init__(self, name: str):
self.name = name
self._catalog: dict[str, LibraryItem] = {} # item_id → LibraryItem
self._members: dict[str, LibraryMember] = {} # member_id → Member
self._observers: list[OverdueObserver] = [] # Overdue notification subscribers
# --- Catalog Management ---
def add_item(self, item: LibraryItem):
self._catalog[item.item_id] = item # Register item in catalog
def search(self, query: str) -> list[LibraryItem]:
# Case-insensitive search across titles
q = query.lower()
return [item for item in self._catalog.values()
if q in item.title.lower()]
# --- Member Management ---
def register_member(self, member: LibraryMember):
self._members[member.member_id] = member # Register new member
# --- Borrowing Operations ---
def checkout(self, member_id: str, item_id: str) -> BorrowingRecord:
member = self._members.get(member_id)
if not member:
raise ValueError(f"Member {member_id} not found")
item = self._catalog.get(item_id)
if not item:
raise ValueError(f"Item {item_id} not found in catalog")
if not member.can_borrow:
raise ValueError(f"{member.name} has reached their borrowing limit")
item.checkout() # Mark item unavailable
record = BorrowingRecord(item, member_id) # Create borrow record
member.add_borrow(record) # Add to member's borrows
print(f"Checked out '{item.title}' to {member.name}. Due: {record.due_date}")
return record
def return_item(self, member_id: str, item_id: str):
member = self._members.get(member_id)
if not member:
raise ValueError(f"Member {member_id} not found")
record = member.complete_return(item_id) # Process return
fine = record.fine_amount
if fine > 0:
print(f"Returned '{record.item.title}'. Late fine: ${fine:.2f}")
else:
print(f"Returned '{record.item.title}'. No fines.")
# --- Observer Pattern Integration ---
def add_observer(self, observer: OverdueObserver):
self._observers.append(observer) # Register notification handler
def check_overdue_items(self):
"""Scan all active borrows and notify observers of overdue items."""
for member in self._members.values():
for record in member.active_borrows:
if record.is_overdue:
for observer in self._observers:
observer.on_overdue(member, record)# --- Setup ---
library = Library("City Public Library")
# Register overdue notification handlers
library.add_observer(EmailOverdueNotifier())
library.add_observer(StaffAlertNotifier())
# Add items to catalog
library.add_item(Book("B001", "Clean Code", 2008, "Robert C. Martin", "978-0132350884"))
library.add_item(Book("B002", "The Pragmatic Programmer", 2019, "Hunt & Thomas", "978-0135957059"))
library.add_item(Magazine("M001", "National Geographic", 2024, "Vol. 245, No. 3"))
library.add_item(DVD("D001", "Inception", 2010, "Christopher Nolan", 148))
# Register members
library.register_member(LibraryMember("M001", "Alice Chen", "alice@example.com"))
library.register_member(PremiumMember("M002", "Bob Smith", "bob@example.com"))
# --- Borrowing ---
library.checkout("M001", "B001")
library.checkout("M001", "M001")
library.checkout("M002", "B002")
library.checkout("M002", "D001")
# --- Search ---
results = library.search("clean")
for item in results:
print(item)
# --- Return with no fine ---
library.return_item("M001", "B001")
# --- Check overdue (in a real scenario, some items would be past due) ---
library.check_overdue_items()Sample Output:
Checked out 'Clean Code' to Alice Chen. Due: 2026-06-24
Checked out 'National Geographic' to Alice Chen. Due: 2026-06-10
Checked out 'The Pragmatic Programmer' to Bob Smith. Due: 2026-06-24
Checked out 'Inception' to Bob Smith. Due: 2026-06-08
Book [B001]: 'Clean Code' (2008) — Checked Out
Returned 'Clean Code'. No fines.| Principle | How This Project Follows It |
|---|---|
| Single Responsibility | LibraryItem handles item data; BorrowingRecord handles transactions; Library coordinates operations |
| Open/Closed | Add new item types (Audiobook, eBook) by subclassing LibraryItem no existing code changes |
| Liskov Substitution | PremiumMember is fully substitutable for LibraryMember all checkout/return operations work identically |
| Interface Segregation | OverdueObserver is small and focused observers only implement what they need |
| Dependency Inversion | Library depends on LibraryItem and LibraryMember abstractions, not concrete Book or PremiumMember |
LibraryItem) with three concrete implementationsPremiumMember extends LibraryMember)Library contains items, members, records, and observers)available, can_borrow, total_fines)You've mastered Object Oriented Programming from classes and inheritance all the way to design patterns and architectural principles. Your next steps:
pytest and mock objectsEvery large system operating systems, databases, web frameworks is built from small, well designed classes. You now have the tools to build them.
University learning companion
Object-Oriented Programming (OOP) · Final Project: Build an OOP-Based Library System
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises