Shared Booknook Data
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.
| Modules | Slice | Shape |
|---|---|---|
| 1 | One book (B06, Frankenstein) and one loan (L04, Jonah's) | Separate variables |
| 2 to 4 | Six books and twelve loans | Lines of text, with dates as shelf day numbers (see below) |
| 5 to 6 | The full catalogue | A list of dictionaries, written out in the lesson |
| 7 | The full catalogue and loans, with real dates | Package files written by the lesson's catch-up cell |
| 8 to 11 | The four data files | Written 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.
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 as0.1are stored slightly wrongly. TODAYis fixed. Code that decides what is overdue takestodayas 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
printshows that the cap is reached at 15 days late.
Output
Wednesday 14 October 2026
cap reached after 15 days lateThe 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.
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_CSVis 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 calleddata, or does nothing if it is already there.write_text(..., encoding="utf-8")saves the text as a file.- The
printcounts the lines and takes one away for the header.
Output
24 books writtenThe 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.
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
MEMBERSis a list of six dictionaries, one per member. Dates are written asYYYY-MM-DDtext, the international standard order (ISO 8601), so they sort correctly as text.- Jonah's
favourite_genresis 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=Falsekeeps any non-English letters as they are.- The file is written the same way as the books file.
Output
6 members writtenThe 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.
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_idandmember_idpoint 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,"", notNone; Module 8 shows how to turn it intoNone. - 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
40 loans writtenTom'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.
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,
backand 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:
| Line | Fault | What a correct line would have |
|---|---|---|
| 4 | Short date 3/9/26: no leading zeros and a two-digit year | 03/09/2026 |
| 5 | Misspelt title Frankenstien | Frankenstein (B06) |
| 6 | Name in lower case with extra spaces, lena | Lena |
| 8 | Returned on 5 September, before it was borrowed on 7 September | A return date on or after the borrow date |
| 11 | An exact copy of line 7 | Nothing: the line should not be there |
- Every title in the notebook, apart from the misspelling, matches a title in
books.csvexactly. 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
12 notebook lines writtenThe 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.
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.
dueadds 14 days to the borrow date.feeturns days late into pence: nothing for zero or fewer days, 20 pence a day, never more than 300.outis every loan with an emptyreturned.overdueis the loans out whose due date is before today.soonis the loans out that are due today or in the next three days.late_returnsis the loans that came back after their due date. Their fees were paid on return and went in the café's charity jar, sojaris the jar's total.- The remaining lines print per-member and per-genre counts and the books that
loans.csvnever lent.
Output
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.
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
limits hold on all 31 days from 14 September to 14 OctoberIn words, the facts every lesson must agree with:
- 9 of 27 copies are out. Kwame has 3 out, the limit, so Module 9's
Librarymust 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.csvnever 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.
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 = 44is Wednesday 14 October as a shelf day.
Output
6 books, 12 loansThis check, for authors, proves the shelf days match the real dates and gives the answers Module 4's lab must print.
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. dayturns an ISO date into a shelf day by counting days since 31 August. The twoassertlines 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
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 0Module 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:
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.
yearis a number; the rest are text in quotation marks. printshows several values on one line, with a space between each.
Output
Frankenstein by Mary Shelley 1818
L04 Jonah Monday 14 September 2026The 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.
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 turningyearandcopiesinto numbers. A lesson that types out the full catalogue should run the same check while it is being written.
Output
catalogue lines match books.csvChecks 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.