Final Capstone: A Community Lending Library · practice
Enforce State and Design Errors
The library refuses things correctly. It just refuses them badly.
Try checking out an item that is already on loan and read what comes back:
ItemUnavailableError: ('b1', 24)
That is Python’s default: with no constructor of its own, an
An exception is an object you designed
Chapter 12 made this
Two audiences need different things from the same error.
A person needs a sentence: item ‘b1’ is on loan until day 24. They should not have to know what order the arguments were in.
A program needs the parts: the interface that says “try again after day 24” needs 24 as a number, not as a substring to be parsed back out of English.
A well-made exception serves both, and the way to do it is a constructor that takes the parts, keeps them as attributes, and builds the sentence itself:
class ItemUnavailableError(LibraryError):
def __init__(self, item_id, due_day):
super().__init__(f"item {item_id!r} is on loan until day {due_day}")
self.item_id = item_id
self.due_day = due_day
super().__init__(...) is what makes str(error) the sentence. The two assignments are what make the parts reachable. Neither substitutes for the other.
Why the call sites do not change
Look at what checkout already writes:
raise ItemUnavailableError(item_id, self._loans[item_id].due_day)
It has been passing the parts all along. It never built a message, and it never should have: a
That is why this lesson touches only errors.py. The information was already flowing; you are giving it somewhere to live.
Why store due_day as an attribute when it already appears in the message?
The family, and what the base class is for
LibraryError stays exactly as it is: a plain subclass of Exception with a docstring and nothing else.
That is not laziness. Its whole job is to be a name you can catch:
try:
library.checkout(item_id, member_id, today)
except LibraryError as error:
print(f"Sorry: {error}")
One handler, every refusal, a sentence for each. A caller who wants to say something more specific catches ItemUnavailableError first and reads error.due_day. Chapter 9’s substitution rule is what makes both work at once: every one of the four is a LibraryError, so catching the base can never miss one.
Adding a fifth kind of refusal later means adding a class. It does not mean revisiting anyone’s except clause.
What to build now
Give each of the four a constructor that takes the parts library.py already passes, builds a readable message from them, and keeps the parts as attributes.
Leave LibraryError alone.
Task
Give the four domain errors real constructors, in errors.py.
Each takes the parts library.py already passes, keeps them as attributes of those names, and builds its own message with super().__init__(...):
UnknownItemError(item_id):"no item with id 'b1'", attributeitem_id.UnknownMemberError(member_id):"no member with id 'm1'", attributemember_id.ItemUnavailableError(item_id, due_day):"item 'b1' is on loan until day 24", attributesitem_idanddue_day.LoanLimitReachedError(member_id, limit):"member 'm1' already holds 2 item(s)", attributesmember_idandlimit.
Use !r for the ids, so the quotes make it obvious where an id starts and ends.
Leave LibraryError exactly as it is. Its job is to be the one name that catches all four.