```python
# custom_errors.py
class CustomTypeError(Exception):
"""Raised when a value is of the wrong type."""
pass
class CustomValueError(Exception):
"""Raised when a value is of correct type but invalid."""
pass
class CustomDateError(Exception):
"""Raised when a date is in wrong format or logically invalid."""
pass
class MissingRequiredFieldError(Exception):
"""Raised when a required field is missing."""
pass
class UserNotFoundError(Exception):
"""Raised when a user does not exist."""
pass
class BookNotFoundError(Exception):
"""Raised when a book does not exist."""
pass
class CustomOperationError(Exception):
"""Raised when an invalid borrow/return operation is attempted."""
pass
class CustomLimitError(Exception):
"""Raised when a borrowing limit is exceeded."""
pass
class CustomKeyError(Exception):
"""Raised when trying to access a non-existent key."""
pass
```
---
### ✅ **Updated `user.py` with Defensive Programming and Assertions**
```python
# user.py
from datetime import datetime
from custom_errors import CustomTypeError, CustomValueError, MissingRequiredFieldError
class User:
VALID_DEPARTMENTS = {"IT", "Business", "Arts", "Science", "Engineering", "Education", "Medicine", "Library"}
def __init__(self, user_id: str, name: str, user_type: str, department: str = "", password: str = ""):
# Type checks
if not isinstance(user_id, str):
raise CustomTypeError(f"User ID must be a string. Got {type(user_id).__name__}.")
if not isinstance(name, str):
raise CustomTypeError(f"Name must be a string. Got {type(name).__name__}.")
if not isinstance(user_type, str):
raise CustomTypeError(f"User type must be a string. Got {type(user_type).__name__}.")
if not isinstance(department, str):
raise CustomTypeError(f"Department must be a string. Got {type(department).__name__}.")
if not isinstance(password, str):
raise CustomTypeError(f"Password must be a string. Got {type(password).__name__}.")
# Required fields
if not user_id.strip():
raise MissingRequiredFieldError("User ID cannot be empty.")
if not name.strip():
raise MissingRequiredFieldError("Name cannot be empty.")
if not user_type.strip():
raise MissingRequiredFieldError("User type cannot be empty.")
if not password.strip():
raise MissingRequiredFieldError("Password cannot be empty.")
# Validate name: each word must be alphabetic only
for word in name.strip().split():
if not word.isalpha():
raise CustomValueError(f"Name contains invalid word '{word}'. Only alphabetic characters are allowed.")
# Department logic
if user_type.lower() in ["student", "staff"]:
if not department.strip():
raise MissingRequiredFieldError(f"Department is required for {user_type}.")
dept = department.strip()
if dept not in self.VALID_DEPARTMENTS:
raise CustomValueError(f"Invalid department: '{dept}'. Valid options: {', '.join(sorted(self.VALID_DEPARTMENTS))}")
else:
if department.strip():
raise CustomValueError(f"Only students and staff can have a department. Found '{department}' for {user_type}.")
# Assign attributes
self.user_id = user_id.strip()
self.name = name.strip()
self.user_type = user_type.strip().lower()
self.department = department.strip() if department else ""
self.password = password # Already checked non-empty above
self.borrowed_books = []
# Assert internal consistency
assert isinstance(self.user_id, str) and len(self.user_id) > 0, "User ID failed post-initialization validation."
assert isinstance(self.name, str) and len(self.name) > 0, "Name failed post-initialization validation."
assert self.user_type in ["student", "staff", "other"], f"Unexpected user_type after init: {self.user_type}"
def can_borrow(self) -> bool:
max_books = 5 if self.user_type == "student" else 10
return len(self.borrowed_books) < max_books
def borrow_book(self, book):
from book import Book # Avoid circular import
if not isinstance(book, Book):
raise CustomTypeError(f"Expected Book object, got {type(book).__name__}.")
if not self.can_borrow():
raise CustomLimitError(f"{self.name} has reached the borrowing limit ({len(self.borrowed_books)} books).")
self.borrowed_books.append(book)
def return_book(self, book):
if book not in self.borrowed_books:
raise CustomOperationError(f"{self.name} does not have book '{book.title}'. Cannot return.")
self.borrowed_books.remove(book)
def __repr__(self):
return f"User(ID={self.user_id}, Name='{self.name}', Type='{self.user_type}', Dept='{self.department}')"
```
---
### ✅ **Updated `book.py` with Defensive Programming and Assertions**
```python
# book.py
from custom_errors import CustomTypeError, CustomValueError, MissingRequiredFieldError
class Book:
VALID_TYPES = ("physical", "online")
def __init__(self, isbn: str, title: str, author: str, year: int, book_type: str, keywords: list = None):
# Type checks
if not isinstance(isbn, str):
raise CustomTypeError(f"ISBN must be a string. Got {type(isbn).__name__}.")
if not isinstance(title, str):
raise CustomTypeError(f"Title must be a string. Got {type(title).__name__}.")
if not isinstance(author, str):
raise CustomTypeError(f"Author must be a string. Got {type(author).__name__}.")
if not isinstance(year, int):
raise CustomTypeError(f"Year must be an integer. Got {type(year).__name__}.")
if not isinstance(book_type, str):
raise CustomTypeError(f"Book type must be a string. Got {type(book_type).__name__}.")
if keywords is not None and not isinstance(keywords, list):
raise CustomTypeError(f"Keywords must be a list or None. Got {type(keywords).__name__}.")
# Required fields
if not isbn.strip():
raise MissingRequiredFieldError("ISBN cannot be empty.")
if not title.strip():
raise MissingRequiredFieldError("Title cannot be empty.")
if not author.strip():
raise MissingRequiredFieldError("Author cannot be empty.")
if year < 1000 or year > datetime.now().year:
raise CustomValueError(f"Year must be between 1000 and current year. Got {year}.")
if book_type.lower() not in self.VALID_TYPES:
raise CustomValueError(f"Invalid book type: '{book_type}'. Must be one of {self.VALID_TYPES}.")
# Keywords validation
keywords = keywords or []
if len(keywords) > 5:
raise CustomLimitError(f"A book can have at most 5 keywords. Got {len(keywords)}.")
for kw in keywords:
if not isinstance(kw, str):
raise CustomTypeError(f"Keyword must be a string. Got {type(kw).__name__}.")
if not kw.strip():
raise CustomValueError("Keywords cannot be empty.")
# Only letters, digits, and hyphens allowed
if not all(c.isalnum() or c == '-' for c in kw):
raise CustomValueError(f"Invalid keyword '{kw}'. Only letters, numbers, and hyphens are allowed.")
# Assign values
self.isbn = isbn.strip()
self.title = title.strip()
self.author = author.strip()
self.year = year
self.type = book_type.lower()
self.keywords = [kw.strip() for kw in keywords]
# Assert internal state
assert isinstance(self.isbn, str) and len(self.isbn) > 0, "ISBN validation failed after assignment."
assert isinstance(self.title, str) and len(self.title) > 0, "Title validation failed after assignment."
assert isinstance(self.author, str) and len(self.author) > 0, "Author validation failed after assignment."
assert self.year >= 1000 and self.year <= datetime.now().year, "Year out of range after assignment."
assert self.type in self.VALID_TYPES, "Book type invalid after assignment."
assert len(self.keywords) <= 5, "Too many keywords stored."
def matches_keyword(self, keyword: str) -> bool:
if not isinstance(keyword, str):
raise CustomTypeError(f"Keyword to match must be a string. Got {type(keyword).__name__}.")
return any(keyword.lower() in kw.lower() for kw in self.keywords)
def __repr__(self):
return f"Book(ISBN={self.isbn}, Title='{self.title}', Author='{self.author}', Year={self.year}, Type='{self.type}')"
```
---
### ✅ **Updated `task4.py` – Robust Main Program with Exception Handling**
```python
# task4.py
import csv
from datetime import datetime
from custom_errors import (
MissingRequiredFieldError, CustomTypeError, CustomValueError,
UserNotFoundError, BookNotFoundError, CustomDateError,
CustomOperationError, CustomLimitError
)
from user import User
from book import Book
class LibrarySystem:
def __init__(self):
self.users = {} # user_id -> User
self.books = {} # isbn -> Book
self.loans = {} # isbn -> (user_id, due_date)
def load_users(self, filename):
try:
with open(filename, newline='', encoding='utf-8') as file:
reader = csv.DictReader(file)
for line_num, row in enumerate(reader, start=2): # Start at 2 because header is line 1
try:
user_id = row.get('user_id', '').strip()
name = row.get('name', '').strip()
user_type = row.get('user_type', '').strip()
department = row.get('department', '').strip()
password = row.get('password', '').strip()
if not all([user_id, name, user_type, password]):
raise MissingRequiredFieldError(f"Missing required field(s) in row {line_num}: {row}")
user = User(user_id, name, user_type, department, password)
self.users[user_id] = user
except (CustomTypeError, CustomValueError, MissingRequiredFieldError) as e:
print(f"[Line {line_num}] Error loading user: {e}")
except Exception as e:
print(f"[Line {line_num}] Unexpected error: {e}")
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
except Exception as e:
print(f"Error reading users file: {e}")
def load_books(self, filename):
try:
with open(filename, newline='', encoding='utf-8') as file:
reader = csv.DictReader(file)
for line_num, row in enumerate(reader, start=2):
try:
isbn = row.get('isbn', '').strip()
title = row.get('title', '').strip()
author = row.get('author', '').strip()
year_str = row.get('year', '').strip()
book_type = row.get('type', '').strip()
keywords_str = row.get('keywords', '').strip()
if not all([isbn, title, author, year_str, book_type]):
raise MissingRequiredFieldError(f"Missing required field(s) in row {line_num}: {row}")
try:
year = int(year_str)
except ValueError:
raise CustomTypeError(f"Year must be an integer. Got '{year_str}'.")
keywords = [kw.strip() for kw in keywords_str.split(",")] if keywords_str else []
book = Book(isbn, title, author, year, book_type, keywords)
self.books[isbn] = book
except (CustomTypeError, CustomValueError, MissingRequiredFieldError, CustomLimitError) as e:
print(f"[Line {line_num}] Error loading book: {e}")
except Exception as e:
print(f"[Line {line_num}] Unexpected error: {e}")
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
except Exception as e:
print(f"Error reading books file: {e}")
def load_loans(self, filename):
try:
with open(filename, newline='', encoding='utf-8') as file:
reader = csv.DictReader(file)
for line_num, row in enumerate(reader, start=2):
try:
user_id = row.get('user_id', '').strip()
isbn = row.get('isbn', '').strip()
due_date_str = row.get('due_date', '').strip()
if not all([user_id, isbn, due_date_str]):
raise MissingRequiredFieldError(f"Missing required loan field(s) in row {line_num}.")
# Validate date
try:
due_date = datetime.strptime(due_date_str, "%Y-%m-%d")
if due_date.date() < datetime.now().date():
print(f"[Warning] Due date expired for loan: {isbn} -> {user_id}")
except ValueError:
raise CustomDateError(f"Invalid due date format: '{due_date_str}'. Use YYYY-MM-DD.")
# Check existence
if user_id not in self.users:
raise UserNotFoundError(f"User ID '{user_id}' not found.")
if isbn not in self.books:
raise BookNotFoundError(f"ISBN '{isbn}' not found.")
# Perform loan action
user = self.users[user_id]
book = self.books[isbn]
if not user.can_borrow():
raise CustomLimitError(f"User '{user.name}' cannot borrow more books.")
user.borrow_book(book)
self.loans[isbn] = (user_id, due_date)
except (UserNotFoundError, BookNotFoundError, CustomDateError,
CustomLimitError, MissingRequiredFieldError, CustomOperationError) as e:
print(f"[Line {line_num}] Error processing loan: {e}")
except Exception as e:
print(f"[Line {line_num}] Unexpected error: {e}")
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
except Exception as e:
print(f"Error reading loans file: {e}")
def return_book(self, user_id: str, isbn: str):
try:
if user_id not in self.users:
raise UserNotFoundError(f"User ID '{user_id}' not found.")
if isbn not in self.books:
raise BookNotFoundError(f"ISBN '{isbn}' not found.")
if isbn not in self.loans:
raise CustomOperationError(f"Book '{isbn}' is not currently on loan.")
user = self.users[user_id]
book = self.books[isbn]
user.return_book(book)
del self.loans[isbn]
print(f"Book '{book.title}' returned successfully by {user.name}.")
except (UserNotFoundError, BookNotFoundError, CustomOperationError) as e:
print(f"Return failed: {e}")
except Exception as e:
print(f"Unexpected error during return: {e}")
def search_books(self, keyword: str):
try:
if not keyword or not isinstance(keyword, str):
raise CustomTypeError("Search keyword must be a non-empty string.")
results = [book for book in self.books.values() if book.matches_keyword(keyword)]
return results
except Exception as e:
print(f"Search error: {e}")
return []
# Example usage
if __name__ == "__main__":
lib = LibrarySystem()
lib.load_users("users.csv")
lib.load_books("books.csv")
lib.load_loans("loans.csv")
print("\n--- Search Results for 'Python' ---")
for book in lib.search_books("Python"):
print(book)
print("\n--- Returning Book ---")
lib.return_book("U003", "9780134685991") # Example return
```
---
### 🔍 Explanation
- **Custom Errors**: Defined in `custom_errors.py`, used consistently across modules.
- **Defensive Input Validation**:
- All inputs are checked for type (`isinstance`) and validity (e.g., department, name).
- Empty passwords, invalid names, or malformed departments trigger appropriate exceptions.
- **Assertions**: Used internally to ensure class invariants (e.g., user type is valid post-init).
- **CSV Loading Resilience**: Each row is processed individually; errors are caught per-line to avoid full system crash.
- **Loan System Integrity**: Validates due dates, existing users/books, and borrowing limits before creating loans.
- **Graceful Degradation**: The system logs errors and continues instead of crashing.
---
### ✅ Summary of Key Improvements
| Feature | Implemented |
|-------|-------------|
| Custom Exceptions | ✅ Used throughout |
| Type Safety | ✅ `isinstance` checks |
| Value Validation | ✅ Names, departments, years, keywords |
| Required Fields | ✅ Enforced via `MissingRequiredFieldError` |
| Max Keywords | ✅ Limited to 5 |
| Loan Integrity | ✅ Checks user/book existence, due date |
| Error Messages | ✅ Descriptive and helpful |
| System Stability | ✅ No crashes on bad data |
---