CoursePython from Scratch to Intermediate · Course guide · part 5 of 75
Part 5 · Course guide

Shared Booknook Data

16 min read·14 Oct 2026

Every module uses the same small set of Booknook records, so you never have to learn a new example. This page is the single source for all of them. Lessons copy only the slice they need.

The people and the loans are made up. Kettle and Page, Amira, Tom and the six members are fictional, and every loan below is simulated. Email addresses use example.com, a domain reserved for examples. The books are real: twenty-four titles old enough to be in the public domain.

For authors: copy these blocks into lessons exactly as written. Do not invent other members, books, loans or dates. If a lesson needs more data, add it in that lesson, say "added in this lesson", and leave this page unchanged. Every block below was run, top to bottom in one session, with Python 3.12.15 and with Python 3.9.6 on 14 October 2026, and gave the same output both times. The outputs shown are copied from that run.

Which lesson uses which slice

The data grows with what you know. A lesson must not use a tool it has not taught yet, so early lessons get the data in simpler shapes.

ModulesSliceShape
1One book (B06, Frankenstein) and one loan (L04, Jonah's)Separate variables
2 to 4Six books and twelve loansLines of text, with dates as shelf day numbers (see below)
5 to 6The full catalogueA list of dictionaries, written out in the lesson
7The full catalogue and loans, with real datesPackage files written by the lesson's catch-up cell
8 to 11The four data filesWritten by a short cell, then read from disk

The rules and today's date

These are the shelf's rules, as numbers. They never change.

python
from datetime import date

LOAN_DAYS = 14             # a loan lasts 14 days
FEE_PER_DAY = 20           # late fee, in pence, for each day after the due date
FEE_CAP = 300              # the most one loan can be charged, in pence
MAX_BOOKS = 3              # the most books one member may have out at once
TODAY = date(2026, 10, 14) # the course's fixed "today"

print(TODAY.strftime("%A %d %B %Y"))
print("cap reached after", FEE_CAP // FEE_PER_DAY, "days late")

Code walkthrough

  • The four capital-letter names are the shelf rules. Capital letters are a Python habit that says "this value should never change" (Module 1 Lesson 3).
  • Money is always a whole number of pence. Pounds appear only when a value is printed, as £3.00. Module 2 Lesson 1 shows why: decimal fractions such as 0.1 are stored slightly wrongly.
  • TODAY is fixed. Code that decides what is overdue takes today as a parameter and is given this date, so every run, on any day, gives the same answer. date.today() asks the computer's clock and is shown once in Module 7, never used in a checked output.
  • A loan is due on the borrow date plus 14 days. It is overdue only when today is after the due date: a book returned on its due date is on time.
  • The second print shows that the cap is reached at 15 days late.

Output

text
Wednesday 14 October 2026
cap reached after 15 days late

The books: data/books.csv

Twenty-four donated books in seven genres. Three popular titles have two copies, so the shelf holds 27 copies in all.

python
from pathlib import Path

BOOKS_CSV = """book_id,title,author,year,genre,copies
B01,Pride and Prejudice,Jane Austen,1813,romance,2
B02,Emma,Jane Austen,1815,romance,1
B03,Sense and Sensibility,Jane Austen,1811,romance,1
B04,Jane Eyre,Charlotte Brontë,1847,romance,1
B05,Wuthering Heights,Emily Brontë,1847,gothic,1
B06,Frankenstein,Mary Shelley,1818,gothic,1
B07,Dracula,Bram Stoker,1897,gothic,1
B08,Strange Case of Dr Jekyll and Mr Hyde,Robert Louis Stevenson,1886,gothic,1
B09,Great Expectations,Charles Dickens,1861,drama,1
B10,A Christmas Carol,Charles Dickens,1843,drama,1
B11,Little Women,Louisa May Alcott,1868,drama,1
B12,Moby-Dick,Herman Melville,1851,adventure,1
B13,Treasure Island,Robert Louis Stevenson,1883,adventure,1
B14,Kidnapped,Robert Louis Stevenson,1886,adventure,1
B15,The Call of the Wild,Jack London,1903,adventure,1
B16,The Adventures of Sherlock Holmes,Arthur Conan Doyle,1892,mystery,2
B17,The Hound of the Baskervilles,Arthur Conan Doyle,1902,mystery,1
B18,The Moonstone,Wilkie Collins,1868,mystery,1
B19,The Time Machine,H. G. Wells,1895,science fiction,1
B20,The War of the Worlds,H. G. Wells,1898,science fiction,1
B21,Alice's Adventures in Wonderland,Lewis Carroll,1865,children,2
B22,The Secret Garden,Frances Hodgson Burnett,1911,children,1
B23,Anne of Green Gables,L. M. Montgomery,1908,children,1
B24,The Wind in the Willows,Kenneth Grahame,1908,children,1
"""

Path("data").mkdir(exist_ok=True)
Path("data/books.csv").write_text(BOOKS_CSV, encoding="utf-8")
print(len(BOOKS_CSV.splitlines()) - 1, "books written")

Code walkthrough

  • BOOKS_CSV is the whole file as one piece of text. The first line is the header, naming the six columns; each line after it is one book.
  • No title contains a comma, so the file needs no quotation marks. Moby-Dick's hyphen and the apostrophe in Alice's title are ordinary characters.
  • The two Brontë names contain ë, a letter outside plain English. That is on purpose: Module 3 uses it to explain Unicode and Module 8 to explain why files are saved as UTF-8.
  • Path("data").mkdir(exist_ok=True) makes a folder called data, or does nothing if it is already there. write_text(..., encoding="utf-8") saves the text as a file.
  • The print counts the lines and takes one away for the header.

Output

text
24 books written

The genres are: romance 4, gothic 4, drama 3, adventure 4, mystery 3, science fiction 2, children 4. Years are the first year of publication in book form.

The members: data/members.json

Six members. Each record holds a list of favourite genres, a list inside a record, which is what makes this file JSON rather than CSV.

python
import json

MEMBERS = [
    {"member_id": "M01", "name": "Priya Nair", "joined": "2026-09-01",
     "email": "priya.nair@example.com", "favourite_genres": ["mystery"]},
    {"member_id": "M02", "name": "Ben Carter", "joined": "2026-09-01",
     "email": "ben.carter@example.com", "favourite_genres": ["adventure", "science fiction"]},
    {"member_id": "M03", "name": "Lena Fischer", "joined": "2026-09-04",
     "email": "lena.fischer@example.com", "favourite_genres": ["romance", "drama"]},
    {"member_id": "M04", "name": "Kwame Asante", "joined": "2026-09-01",
     "email": "kwame.asante@example.com", "favourite_genres": ["adventure", "mystery", "gothic"]},
    {"member_id": "M05", "name": "Sofia Marin", "joined": "2026-09-21",
     "email": "sofia.marin@example.com", "favourite_genres": ["children", "romance"]},
    {"member_id": "M06", "name": "Jonah Webb", "joined": "2026-09-02",
     "email": "jonah.webb@example.com", "favourite_genres": []},
]

Path("data/members.json").write_text(json.dumps(MEMBERS, indent=2, ensure_ascii=False), encoding="utf-8")
print(len(MEMBERS), "members written")

Code walkthrough

  • MEMBERS is a list of six dictionaries, one per member. Dates are written as YYYY-MM-DD text, the international standard order (ISO 8601), so they sort correctly as text.
  • Jonah's favourite_genres is an empty list. That is on purpose: it is the course's example of a field that exists but holds nothing.
  • json.dumps(..., indent=2) turns the list into JSON text, indented two spaces so a person can read it. ensure_ascii=False keeps any non-English letters as they are.
  • The file is written the same way as the books file.

Output

text
6 members written

The loans: data/loans.csv

Forty loans, from Monday 14 September to Tuesday 13 October 2026, when Amira kept the records in a spreadsheet. An empty returned column means the book is still out.

python
LOANS_CSV = """loan_id,book_id,member_id,borrowed,returned
L01,B01,M01,2026-09-14,2026-09-25
L02,B16,M02,2026-09-14,2026-10-02
L03,B17,M04,2026-09-14,2026-09-23
L04,B06,M06,2026-09-14,
L05,B16,M01,2026-09-15,2026-09-29
L06,B02,M03,2026-09-15,2026-09-27
L07,B13,M02,2026-09-16,2026-10-03
L08,B19,M04,2026-09-16,2026-09-21
L09,B07,M04,2026-09-17,2026-10-03
L10,B03,M03,2026-09-18,2026-10-07
L11,B18,M01,2026-09-19,2026-10-01
L12,B04,M06,2026-09-19,2026-09-28
L13,B20,M02,2026-09-21,
L14,B22,M05,2026-09-21,2026-10-02
L15,B11,M03,2026-09-23,2026-10-06
L16,B14,M04,2026-09-23,2026-10-05
L17,B08,M05,2026-09-24,2026-09-27
L18,B23,M05,2026-09-25,2026-10-08
L19,B10,M06,2026-09-25,2026-10-06
L20,B05,M01,2026-09-26,2026-10-06
L21,B01,M04,2026-09-26,
L22,B24,M05,2026-09-28,2026-10-09
L23,B04,M03,2026-09-29,
L24,B16,M01,2026-10-01,
L25,B19,M06,2026-10-01,2026-10-10
L26,B21,M05,2026-10-03,
L27,B09,M02,2026-10-05,2026-10-12
L28,B13,M04,2026-10-06,
L29,B17,M01,2026-10-07,2026-10-12
L30,B01,M03,2026-10-07,2026-10-13
L31,B14,M02,2026-10-08,2026-10-09
L32,B12,M06,2026-10-08,2026-10-13
L33,B08,M03,2026-10-09,2026-10-13
L34,B02,M05,2026-10-09,
L35,B11,M01,2026-10-10,2026-10-13
L36,B23,M02,2026-10-10,2026-10-13
L37,B18,M04,2026-10-11,2026-10-13
L38,B07,M05,2026-10-12,2026-10-13
L39,B22,M06,2026-10-12,2026-10-13
L40,B05,M04,2026-10-13,
"""

Path("data/loans.csv").write_text(LOANS_CSV, encoding="utf-8")
print(len(LOANS_CSV.splitlines()) - 1, "loans written")

Code walkthrough

  • One line per loan, in the order the books were borrowed. book_id and member_id point to the other two files.
  • A line ending in a comma, such as L04,B06,M06,2026-09-14,, has an empty fifth column: the book has not come back. Reading that column gives an empty string, "", not None; Module 8 shows how to turn it into None.
  • The loans were planned so that no book is ever lent more times than it has copies, no member ever has more than three books out, and Sofia borrows nothing before she joins on 21 September. The limits check below proves all three.

Output

text
40 loans written

Tom's notebook: data/tom_notebook.txt

Twelve lines Tom typed up from his paper notebook, covering 1 to 13 September, before the spreadsheet. Every loan in it was returned by 13 September.

python
TOM_NOTEBOOK = """01/09/2026 - Pride and Prejudice - Priya - back 08/09/2026
01/09/2026 - Treasure Island - Kwame - back 07/09/2026
02/09/2026 - Dracula - Jonah - back 11/09/2026
3/9/26 - The Time Machine - Ben - back 10/09/2026
03/09/2026 - Frankenstien - Ben - back 10/09/2026
04/09/2026 - Emma -   lena   - back 12/09/2026
05/09/2026 - The Hound of the Baskervilles - Priya - back 12/09/2026
07/09/2026 - Kidnapped - Kwame - back 05/09/2026
08/09/2026 - Little Women - Lena - back 13/09/2026
09/09/2026 - The Call of the Wild - Jonah - back 13/09/2026
05/09/2026 - The Hound of the Baskervilles - Priya - back 12/09/2026
10/09/2026 - The Secret Garden - Kwame - back 13/09/2026
"""

Path("data/tom_notebook.txt").write_text(TOM_NOTEBOOK, encoding="utf-8")
print(len(TOM_NOTEBOOK.splitlines()), "notebook lines written")

Code walkthrough

  • Each line is meant to read: borrow date, title, first name, back and the return date, separated by space-hyphen-space. Tom's dates are day first, then month, then a four-digit year.
  • The notebook has exactly five faults, each used by a named lesson. Do not add or fix any:
LineFaultWhat a correct line would have
4Short date 3/9/26: no leading zeros and a two-digit year03/09/2026
5Misspelt title FrankenstienFrankenstein (B06)
6Name in lower case with extra spaces, lena Lena
8Returned on 5 September, before it was borrowed on 7 SeptemberA return date on or after the borrow date
11An exact copy of line 7Nothing: the line should not be there
  • Every title in the notebook, apart from the misspelling, matches a title in books.csv exactly. Every name is a member's first name. Sofia does not appear: she had not joined yet.
  • Module 3 cleans these lines by hand with string methods; Module 8 parses the whole file and collects the five faults into a problem list; the capstone imports the clean lines.

Output

text
12 notebook lines written

The expected answers on Wednesday 14 October (for authors)

This block reads the four files back and computes the answers every module gets to in its own way. Lessons must arrive at these numbers; if a lesson's code gives a different one, the lesson is wrong. It uses tools from Modules 5 to 8, so it is for authors, not for learners in early modules.

python
import csv
from datetime import timedelta
from collections import Counter

books = {b["book_id"]: b for b in csv.DictReader(open("data/books.csv", encoding="utf-8"))}
members = {m["member_id"]: m for m in json.load(open("data/members.json", encoding="utf-8"))}
loans = list(csv.DictReader(open("data/loans.csv", encoding="utf-8")))

def due(loan):
    return date.fromisoformat(loan["borrowed"]) + timedelta(days=LOAN_DAYS)

def fee(days_late):
    return min(max(days_late, 0) * FEE_PER_DAY, FEE_CAP)

out = [l for l in loans if l["returned"] == ""]
overdue = [l for l in out if TODAY > due(l)]
soon = [l for l in out if 0 <= (due(l) - TODAY).days <= 3]
late_returns = [l for l in loans if l["returned"] and date.fromisoformat(l["returned"]) > due(l)]
jar = sum(fee((date.fromisoformat(l["returned"]) - due(l)).days) for l in late_returns)

print("books out:", len(out), "of", sum(int(b["copies"]) for b in books.values()), "copies")
for l in overdue:
    late = (TODAY - due(l)).days
    print("overdue:", l["loan_id"], members[l["member_id"]]["name"], books[l["book_id"]]["title"], late, "days", fee(late), "p")
print("owed on overdue loans:", sum(fee((TODAY - due(l)).days) for l in overdue), "p")
print("due within 3 days:", [(l["loan_id"], str(due(l))) for l in soon])
print("late returns:", [l["loan_id"] for l in late_returns], "fee jar:", jar, "p")
print("out per member:", dict(Counter(members[l["member_id"]]["name"] for l in out)))
print("loans per member:", dict(Counter(l["member_id"] for l in loans)))
print("never lent:", [b for b in books if b not in {l["book_id"] for l in loans}])
print("per genre:", dict(Counter(b["genre"] for b in books.values())))

Code walkthrough

  • The first three lines read the files back: the books and members into dictionaries keyed by their ids, and the loans into a list of dictionaries, one per row.
  • due adds 14 days to the borrow date. fee turns days late into pence: nothing for zero or fewer days, 20 pence a day, never more than 300.
  • out is every loan with an empty returned. overdue is the loans out whose due date is before today. soon is the loans out that are due today or in the next three days.
  • late_returns is the loans that came back after their due date. Their fees were paid on return and went in the café's charity jar, so jar is the jar's total.
  • The remaining lines print per-member and per-genre counts and the books that loans.csv never lent.

Output

text
books out: 9 of 27 copies
overdue: L04 Jonah Webb Frankenstein 16 days 300 p
overdue: L13 Ben Carter The War of the Worlds 9 days 180 p
overdue: L21 Kwame Asante Pride and Prejudice 4 days 80 p
overdue: L23 Lena Fischer Jane Eyre 1 days 20 p
owed on overdue loans: 580 p
due within 3 days: [('L24', '2026-10-15'), ('L26', '2026-10-17')]
late returns: ['L02', 'L07', 'L09', 'L10'] fee jar: 280 p
out per member: {'Jonah Webb': 1, 'Ben Carter': 1, 'Kwame Asante': 3, 'Lena Fischer': 1, 'Priya Nair': 1, 'Sofia Marin': 2}
loans per member: {'M01': 7, 'M02': 6, 'M04': 8, 'M06': 6, 'M03': 6, 'M05': 7}
never lent: ['B15']
per genre: {'romance': 4, 'gothic': 4, 'drama': 3, 'adventure': 4, 'mystery': 3, 'science fiction': 2, 'children': 4}

The next block, also for authors, proves the limits the loans were planned around.

python
def held_on(day):
    return [l for l in loans if date.fromisoformat(l["borrowed"]) <= day
            and (l["returned"] == "" or date.fromisoformat(l["returned"]) > day)]

for n in range(31):
    day = date(2026, 9, 14) + timedelta(days=n)
    held = held_on(day)
    for b in books:
        assert sum(l["book_id"] == b for l in held) <= int(books[b]["copies"]), (day, b)
    for m in members:
        assert sum(l["member_id"] == m for l in held) <= MAX_BOOKS, (day, m)
for l in loans:
    assert l["borrowed"] >= members[l["member_id"]]["joined"], l["loan_id"]
    assert l["returned"] == "" or l["returned"] >= l["borrowed"], l["loan_id"]
print("limits hold on all", n + 1, "days from 14 September to 14 October")

Code walkthrough

  • held_on(day) lists the loans out at the end of a given day. A book returned and borrowed again on the same day counts once, because the return is counted first.
  • The loop walks the 31 days from 14 September to 14 October. On each day no book is out more times than it has copies and no member has more than three books.
  • The last loop checks that nobody borrows before joining (ISO dates compare correctly as text) and that no return comes before its borrow date. Tom's notebook breaks that last rule; the clean file does not.

Output

text
limits hold on all 31 days from 14 September to 14 October

In words, the facts every lesson must agree with:

  • 9 of 27 copies are out. Kwame has 3 out, the limit, so Module 9's Library must refuse him a fourth.
  • 4 loans are overdue: Jonah's Frankenstein (L04, 16 days, capped at 300p), Ben's The War of the Worlds (L13, 9 days, 180p, the "9 days late" of Module 2), Kwame's Pride and Prejudice (L21, 4 days, 80p) and Lena's Jane Eyre (L23, 1 day, 20p). Together they owe 580p (£5.80).
  • 2 loans are due within 3 days: Priya's The Adventures of Sherlock Holmes (L24, due Thursday 15 October) and Sofia's Alice's Adventures in Wonderland (L26, due Saturday 17 October).
  • The fee jar holds 280p (£2.80) from four late returns: L02 and L07 (both Ben's), L09 (Kwame) and L10 (Lena).
  • Priya is never late. L05, Priya's first Sherlock Holmes, came back on its due date, 29 September, and is on time: the course's example of the boundary.
  • Kwame borrows the most (8 loans). The Call of the Wild (B15) is the only book loans.csv never lent (Tom's notebook shows one loan of it in September).

The early slice: six books and twelve loans (Modules 2 to 4)

Modules 2 to 4 come before lists, dictionaries and the datetime module, so they cannot use the files above. They use six books and the twelve loans of those books, written as lines of text, with every date as a shelf day: the day's number counted from 1 September 2026. A September date is its own day number (14 September is day 14); an October date is 30 plus the day (14 October is day 44). The due day is the borrow day plus 14, and today is day 44.

python
SIX_BOOKS = """B01,Pride and Prejudice,Jane Austen,2
B04,Jane Eyre,Charlotte Brontë,1
B06,Frankenstein,Mary Shelley,1
B13,Treasure Island,Robert Louis Stevenson,1
B16,The Adventures of Sherlock Holmes,Arthur Conan Doyle,2
B20,The War of the Worlds,H. G. Wells,1"""

TWELVE_LOANS = """L01,B01,Priya,14,25
L02,B16,Ben,14,32
L04,B06,Jonah,14,
L05,B16,Priya,15,29
L07,B13,Ben,16,33
L12,B04,Jonah,19,28
L13,B20,Ben,21,
L21,B01,Kwame,26,
L23,B04,Lena,29,
L24,B16,Priya,31,
L28,B13,Kwame,36,
L30,B01,Lena,37,43"""

TODAY_DAY = 44
print(len(SIX_BOOKS.splitlines()), "books,", len(TWELVE_LOANS.splitlines()), "loans")

Code walkthrough

  • Each book line is id, title, author and copies. Each loan line is loan id, book id, the member's first name, borrow day and return day. An empty return day means the book is still out, which gives Module 4 its first truthiness example.
  • The loan ids are the same as in loans.csv, so a learner meets L04 and L13 again in Module 8 with real dates.
  • The slice holds all four overdue loans, one of the two due soon (L24), one out and not yet due (L28), two late returns (L02, L07) and the on-the-due-date return (L05).
  • TODAY_DAY = 44 is Wednesday 14 October as a shelf day.

Output

text
6 books, 12 loans

This check, for authors, proves the shelf days match the real dates and gives the answers Module 4's lab must print.

python
for line in TWELVE_LOANS.splitlines():
    loan_id, book_id, name, borrowed, returned = line.split(",")
    real = next(l for l in loans if l["loan_id"] == loan_id)
    day = lambda iso: date.fromisoformat(iso).toordinal() - date(2026, 8, 31).toordinal()
    assert int(borrowed) == day(real["borrowed"]) and book_id == real["book_id"]
    assert returned == (str(day(real["returned"])) if real["returned"] else "")
    due_day = int(borrowed) + LOAN_DAYS
    if returned:
        late = int(returned) - due_day
        status = "returned late" if late > 0 else "returned on time"
    else:
        late = TODAY_DAY - due_day
        status = "overdue" if late > 0 else ("due soon" if -late <= 3 else "out")
    print(loan_id, name, "due day", due_day, status, fee(late) if late > 0 else 0)

Code walkthrough

  • The loop splits each loan line into its five parts and finds the same loan in loans.csv.
  • day turns an ISO date into a shelf day by counting days since 31 August. The two assert lines stop the block if any borrow day, return day or book id disagrees with the real file. None does.
  • The rest decides each loan's status the way Module 4's lab does and prints the fee in pence.

Output

text
L01 Priya due day 28 returned on time 0
L02 Ben due day 28 returned late 80
L04 Jonah due day 28 overdue 300
L05 Priya due day 29 returned on time 0
L07 Ben due day 30 returned late 60
L12 Jonah due day 33 returned on time 0
L13 Ben due day 35 overdue 180
L21 Kwame due day 40 overdue 80
L23 Lena due day 43 overdue 20
L24 Priya due day 45 due soon 0
L28 Kwame due day 50 out 0
L30 Lena due day 51 returned on time 0

Module 4's lab total: the overdue fees in the slice are 300 + 180 + 80 + 20 = 580p, the same as the full file, because all four overdue loans are in it. Module 2's lab works the fee for six of these loans, one at a time: L02, L04, L05, L07, L13 and L23.

Module 1: one book, one loan

Module 1 has only variables. Its lab holds one book and one loan, Jonah's copy of Frankenstein, the loan the course follows furthest:

python
title = "Frankenstein"
author = "Mary Shelley"
year = 1818
loan_id = "L04"
member = "Jonah"
borrowed = "Monday 14 September 2026"
print(title, "by", author, year)
print(loan_id, member, borrowed)

Code walkthrough

  • Six variables, each a name for one value. year is a number; the rest are text in quotation marks.
  • print shows several values on one line, with a space between each.

Output

text
Frankenstein by Mary Shelley 1818
L04 Jonah Monday 14 September 2026

The catalogue as a list of dictionaries (Modules 5 and 6)

From Module 5, lessons write the books out in full. Use this shape, with the same keys as the CSV header and year and copies as numbers. A lesson that needs only a few books copies only those lines.

python
CATALOGUE = [
    {"book_id": "B01", "title": "Pride and Prejudice", "author": "Jane Austen", "year": 1813, "genre": "romance", "copies": 2},
    {"book_id": "B04", "title": "Jane Eyre", "author": "Charlotte Brontë", "year": 1847, "genre": "romance", "copies": 1},
    {"book_id": "B06", "title": "Frankenstein", "author": "Mary Shelley", "year": 1818, "genre": "gothic", "copies": 1},
]
# ... and so on for all 24 books, in book_id order

for book in CATALOGUE:
    row = books[book["book_id"]]
    assert book == {**row, "year": int(row["year"]), "copies": int(row["copies"])}
print("catalogue lines match books.csv")

Code walkthrough

  • Each book is one dictionary. The keys are the CSV column names, so the same names work in every module.
  • The check, for authors, compares each written-out book with the row read from books.csv, after turning year and copies into numbers. A lesson that types out the full catalogue should run the same check while it is being written.

Output

text
catalogue lines match books.csv

Checks still open (for authors)

  • The publication years come from the planning notes and have not yet been checked against a cited catalogue (for example the British Library catalogue). Check each year before a lesson states it as a fact, and record the source here.
  • The fee jar total counts only fees paid on returned loans. Fees on the four overdue loans (580p) are owed, not yet in the jar. Lessons must keep the two apart.