0%

Final Capstone: A Community Lending Library · practice

Yield Overdue Loans

One operation left, and the contract describes it differently from the other two:

overdue_loans(on_day) is an or generator

available_items() and active_loans() both return . This one is allowed not to, and the difference is worth understanding rather than just complying with.

What a generator changes

Chapter 11 built this idea. A generator does not run when you call it; it hands back something that runs a piece at a time, each time you ask for the next .

    def overdue_loans(self, on_day):
        for loan in self._loans.values():
            if loan.is_overdue(on_day):
                yield loan

One yield turns the whole into a generator function. There is no list anywhere, and nothing has been examined at the moment it returns.

For an overdue report that matters in a way the other two do not. Most of the time the answer is empty, and a caller often wants only the first few:

    for loan in library.overdue_loans(today):
        print(f"{loan.item_id} was due on day {loan.due_day}")
        break

With a list, every loan is inspected before the first line prints. With a generator, loans are inspected until the first overdue one is found. If none is overdue, it still examines every active loan, but it never builds a result list.

Where the rule lives

Notice what the is:

            if loan.is_overdue(on_day):

not if on_day > loan.due_day. Lesson 4 put that comparison on Loan, and this is the payoff: the > versus >= decision exists in exactly one place. Had it been written out here, there would now be two places to be wrong, and they would be wrong in different ways eventually.

The honest cost

A generator over self._loans is live. It reads the as it goes, so a caller who returns an item while iterating will get a RuntimeError about the dictionary changing size.

That is a real difference from active_loans(), which copies. It is not a reason to avoid generators, and it is a reason to know which one you are handing out: list(library.overdue_loans(today)) produces a snapshot when consumed before changing the library, and choosing that is the caller’s decision to make rather than yours to force.

Why can active_loans() be iterated while items are returned, when overdue_loans() cannot?

Four files, and why these four

The project is finished after this lesson, so it is worth looking at the shape it landed in.

models.py is the vocabulary: what the program is about. It imports nothing from the rest of the project.

errors.py is what the library refuses. It imports nothing at all.

library.py is the behavior, and it imports both.

main.py is the program, and it imports whatever it needs to run.

Every arrow points the same way, from the program towards the vocabulary, and none points back. That is the shape Chapter 10 argued for, and the test of it is simple: models.py can be understood, tested, and reused without any of the others. That would not be true of any arrangement where the domain knew about the library holding it.

Task

Implement overdue_loans in library.py, the last operation in the contract.

overdue_loans(on_day) yields every active loan that is late on that day, one at a time. Use yield rather than building a : an overdue report is usually empty, and a caller often wants only the first few.

Ask each loan whether it is overdue rather than comparing days here. Loan.is_overdue already owns that decision, and one place is where it should stay.

Nothing else in library.py needs touching. This is the file’s last change.