Final Capstone: A Community Lending Library · practice
Implement the Main Behavior-rich Object
This is the
checkout is where every rule in the contract meets at once: the item has to exist, the member has to exist, the item has to be free, and the member has to be under their limit. Only then does a loan appear.
Refuse first, change last
The order inside the method is not arbitrary.
def checkout(self, item_id, member_id, checkout_day):
if item_id not in self._items:
raise UnknownItemError(...)
if member_id not in self._members:
raise UnknownMemberError(...)
if item_id in self._loans:
raise ItemUnavailableError(...)
...
# only now does anything change
Every check happens before any change. That is worth stating as a rule, because the alternative fails in a way that is genuinely hard to find: a method that records the loan, then discovers the member is over their limit, and raises. The caller sees an
Chapter 5 called this changing related state together, and gave the same advice from the other end: an
Four questions, four different errors
The seeded errors.py gives you a small family:
LibraryError
├── UnknownItemError
├── UnknownMemberError
├── ItemUnavailableError
└── LoanLimitReachedError
Chapter 12 argued for exactly this shape. A caller who only wants to know that something went wrong catches LibraryError. A caller building a user interface wants to say “that one is already out, try next week” for one case and “you have too many out” for another, and it can only do that if the four are distinguishable.
Resist the temptation to collapse them. “Cannot check out” is one message for four different situations, and the person reading it has to guess which.
Counting what a member has out
The loan limit needs a count, and the count is not stored anywhere. That is on purpose: Lesson 2 kept loans off Member so there would be exactly one place that knows them.
So the library counts:
in_hand = 0
for loan in self._loans.values():
if loan.member_id == member_id:
in_hand = in_hand + 1
Deriving it rather than storing it means it cannot go stale. A stored counter is a second source of truth that has to be adjusted on every checkout and every return, and eventually one path forgets.
Why count a member's loans from self._loans instead of keeping a running total on Member?
The loan period, for now
A loan needs a due day, so checkout has to decide how long the item may be kept.
Give it a single loan period for the moment, as a constant at the top of the file:
LOAN_PERIOD_DAYS = 14
You already know this is wrong. A reference book and a board game do not get the same two weeks, and Lesson 7 is where that gets fixed properly. Writing the simple version first is deliberate: it makes the method work end to end, and it puts the decision in one obvious place so that moving it is a small, safe change rather than an excavation.
Returning an item
return_item(item_id) is the mirror. There is one thing to refuse (an item that is not currently out) and one thing to do: remove the loan and hand it back, so the caller can report what was returned.
Removing it is what makes the item available again. There is no separate “available” flag, and there should not be: a flag and a loan record are two ways of saying the same thing, and Lesson 6 shows how to answer the availability question from the loans alone.
Task
Implement checkout and return_item in library.py.
Add LOAN_PERIOD_DAYS = 14 at the top of the file, and import the four specific errors alongside LibraryError.
checkout(item_id, member_id, checkout_day) refuses, in this order, before changing anything:
an unknown
item_idraisesUnknownItemError(item_id)an unknown
member_idraisesUnknownMemberError(member_id)an item that is already on loan raises
ItemUnavailableError(item_id, due_day), using its existing loana member already holding their
loan_limitraisesLoanLimitReachedError(member_id, limit), using that member’s limit
Pass those values separately rather than building a message here. The seeded Loan from models.
Otherwise it records a Loan due LOAN_PERIOD_DAYS after checkout_day, stores it under the item’s id, and returns it.
return_item(item_id) raises LibraryError when that item is not currently on loan. Otherwise it removes the loan and returns it.
Count a member’s current loans from the stored loans. Do not add a counter to Member.