This session builds the Library Information System specified in Session 19 and has the same user validate it. The deliverable is lis.py, a console program with a librarian menu and a member menu and JSON persistence, that implements every Must-have requirement LIS-FR-1 to LIS-FR-9 including the fine rule of Rs 2 per day after 14 days. The record also contains a real sample run, a table tracing each requirement to the function that implements it, the validation record signed off by the Session 19 interviewee, and the list of Should-have items left out.
Objectives
Do not copy. Read for understanding and the viva- Implement every Must requirement from Session 19 and nothing that is not in the requirements list.
- Keep data between runs with the standard library only (LIS-NFR-1, LIS-NFR-2).
- Show traceability from requirement id to function so a reviewer can check coverage.
- Validate the system with the user who gave the requirements and record their verdicts.
Problem Statement
Write in lab recordSessions 19 and 20: Assume that you interested in developing a “Library Information System (LIS)”. Visit any Library. As a visitor of Library, make a list of requirements that need to be fulfilled by LIS. Now, develop Software for LIS. Ensure yourself that LIS developed by you is fulfilling the requirements. Preferably, try to obtain requirements for LIS from any person who visits a library, develop LIS and then get it validated by him/her.
Concept
Do not copy. Read for understanding and the vivaOne function per requirement
Each LIS-FR id from Session 19 maps to one function whose docstring names the id. register_member is LIS-FR-1, issue_book is LIS-FR-4, fine_for is LIS-FR-6, and so on. When a requirement changes, there is one place to edit, and the traceability table writes itself.
Data model
Four things are stored: members, books, loans, and the next free id numbers. A loan is a separate record rather than a field on the book, because a book has many loans over its life and the fine report needs all of them. A reservation is a queue of member ids on the book, in the order they asked, which is exactly the slip inside the register that observation O7 described.
Member(member_id, name, phone, joined)
|
| borrows 0..n
v
Loan(book, member, issued, due, returned, fine)
^
| of 1
|
Book(acc_no, title, author, subject, status, reserved_by[])The whole structure is one Python dictionary saved to lis_data.json after every action, so a power cut loses at most the action in progress.
Dates and the fine rule
Dates are ISO strings in the file and date objects in memory. Issue date defaults to today but can be typed, so the librarian can enter a back-dated issue during validation and see a fine without waiting 15 days. The fine is max(0, days late) * 2 where days late is return date minus due date, and due date is issue date plus 14 days.
Verification before validation
Before showing the user, the developer runs the scripted test below and checks every printed number by hand. Validation with the user then checks that the behaviour is what they wanted, not whether the arithmetic is right.
Program
Write in lab record"""Library Information System (LIS), Session 20.
Console program, Python 3 standard library only, JSON persistence in
lis_data.json next to this file. Implements the Must-have requirements
LIS-FR-1 to LIS-FR-8 recorded in Session 19:
members, catalogue, search, issue (max 3 books, 14 days), return,
fine at 2 rupees per day beyond 14 days, reservation queue, reports.
Dates are entered as YYYY-MM-DD; blank means today, so the librarian can
back-date an issue to demonstrate the fine rule.
"""
import json
import os
from datetime import date, timedelta
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"lis_data.json")
LOAN_DAYS = 14
FINE_PER_DAY = 2
MAX_BOOKS = 3
# ---------------------------------------------------------------- storage
def load():
if os.path.exists(DATA_FILE):
with open(DATA_FILE) as f:
return json.load(f)
return {"members": {}, "books": {}, "loans": [], "next_member": 1,
"next_book": 1}
def save(db):
with open(DATA_FILE, "w") as f:
json.dump(db, f, indent=1)
def ask_date(prompt):
"""Read a YYYY-MM-DD date; blank returns today."""
while True:
text = input(prompt).strip()
if not text:
return date.today()
try:
return date.fromisoformat(text)
except ValueError:
print(" use YYYY-MM-DD")
def fine_for(due, returned):
"""LIS-FR-6: 2 rupees per day after the due date, else 0."""
late = (returned - date.fromisoformat(due)).days
return max(0, late) * FINE_PER_DAY
def open_loan(db, acc):
"""The unreturned loan record of a book, or None."""
for loan in db["loans"]:
if loan["book"] == acc and loan["returned"] is None:
return loan
return None
# ------------------------------------------------------------- librarian
def add_book(db):
"""LIS-FR-2: add a title to the catalogue with a new accession number."""
acc = f"B{db['next_book']:03d}"
book = {"title": input("Title: ").strip(),
"author": input("Author: ").strip(),
"subject": input("Subject: ").strip(),
"status": "available", "reserved_by": []}
if not book["title"]:
print(" title is required")
return
db["books"][acc] = book
db["next_book"] += 1
print(f" added {acc}: {book['title']}")
def register_member(db):
"""LIS-FR-1: register a member and give a member id."""
name = input("Name: ").strip()
phone = input("Phone: ").strip()
if not name or not phone.isdigit() or len(phone) != 10:
print(" name required, phone must be 10 digits")
return
mid = f"M{db['next_member']:03d}"
db["members"][mid] = {"name": name, "phone": phone,
"joined": date.today().isoformat()}
db["next_member"] += 1
print(f" registered {mid}: {name}")
def issue_book(db):
"""LIS-FR-4: issue an available book to a member for 14 days."""
mid = input("Member id: ").strip()
acc = input("Accession no: ").strip()
member, book = db["members"].get(mid), db["books"].get(acc)
if not member or not book:
print(" unknown member or book")
return
if book["status"] == "issued":
print(" already issued; member may reserve it")
return
if book["reserved_by"] and book["reserved_by"][0] != mid:
print(f" held for reservation by {book['reserved_by'][0]}")
return
held = sum(1 for l in db["loans"]
if l["member"] == mid and l["returned"] is None)
if held >= MAX_BOOKS:
print(f" member already holds {MAX_BOOKS} books")
return
issued = ask_date("Issue date (blank = today): ")
due = issued + timedelta(days=LOAN_DAYS)
db["loans"].append({"book": acc, "member": mid, "issued":
issued.isoformat(), "due": due.isoformat(),
"returned": None, "fine": 0})
book["status"] = "issued"
if book["reserved_by"] and book["reserved_by"][0] == mid:
book["reserved_by"].pop(0)
print(f" issued {acc} to {member['name']}, due {due}")
def return_book(db):
"""LIS-FR-5 and LIS-FR-6: return a book and charge any fine."""
acc = input("Accession no: ").strip()
loan = open_loan(db, acc)
if not loan:
print(" that book is not issued")
return
returned = ask_date("Return date (blank = today): ")
fine = fine_for(loan["due"], returned)
loan["returned"], loan["fine"] = returned.isoformat(), fine
book = db["books"][acc]
book["status"] = "available"
print(f" returned {acc}; due was {loan['due']}, fine Rs {fine}")
if book["reserved_by"]:
who = db["members"][book["reserved_by"][0]]["name"]
print(f" reserved: keep aside for {book['reserved_by'][0]} ({who})")
def reports(db):
"""LIS-FR-8: books on loan, overdue list, fines collected."""
today = date.today()
print(" Books on loan:")
for l in db["loans"]:
if l["returned"] is None:
flag = "OVERDUE" if date.fromisoformat(l["due"]) < today else ""
print(f" {l['book']} {db['books'][l['book']]['title']:<28}"
f" {l['member']} due {l['due']} {flag}")
total = sum(l["fine"] for l in db["loans"])
print(f" Fines collected: Rs {total}")
print(f" Members: {len(db['members'])}, titles: {len(db['books'])}")
# ---------------------------------------------------------------- member
def search(db):
"""LIS-FR-3: search by any part of title, author or subject."""
q = input("Search text: ").strip().lower()
hits = [(acc, b) for acc, b in db["books"].items()
if q in (b["title"] + b["author"] + b["subject"]).lower()]
if not hits:
print(" no matching books")
for acc, b in hits:
print(f" {acc} {b['title']:<28} {b['author']:<18} {b['status']}")
def reserve(db, mid):
"""LIS-FR-7: queue for a book that is currently issued."""
acc = input("Accession no: ").strip()
book = db["books"].get(acc)
if not book:
print(" unknown book")
elif book["status"] != "issued":
print(" book is available, ask the librarian to issue it")
elif mid in book["reserved_by"] or open_loan(db, acc)["member"] == mid:
print(" you already hold or reserved this book")
else:
book["reserved_by"].append(mid)
print(f" reserved; you are number {len(book['reserved_by'])}")
def my_books(db, mid):
"""LIS-FR-5 support: what a member holds and the fine if returned now."""
today = date.today()
for l in db["loans"]:
if l["member"] == mid and l["returned"] is None:
print(f" {l['book']} {db['books'][l['book']]['title']} "
f"due {l['due']} fine now Rs {fine_for(l['due'], today)}")
print(f" total fines paid so far: Rs "
f"{sum(l['fine'] for l in db['loans'] if l['member'] == mid)}")
# ------------------------------------------------------------------ menus
def run_menu(title, actions):
"""Print a numbered menu until the user picks 0. Saves after each."""
while True:
print(f"\n{title}: " + " ".join(f"{i} {name}" for i, (name, _) in
enumerate(actions, 1)) + " 0 Back")
choice = input("Choice: ").strip()
if choice == "0":
return
if choice.isdigit() and 1 <= int(choice) <= len(actions):
actions[int(choice) - 1][1]()
save(DB)
else:
print(" unknown choice")
def librarian_menu():
run_menu("Librarian", [("Add book", lambda: add_book(DB)),
("Register member", lambda: register_member(DB)),
("Issue", lambda: issue_book(DB)),
("Return", lambda: return_book(DB)),
("Reports", lambda: reports(DB))])
def member_menu():
mid = input("Member id: ").strip()
if mid not in DB["members"]:
print(" unknown member id")
return
print(f" welcome {DB['members'][mid]['name']}")
run_menu("Member", [("Search", lambda: search(DB)),
("Reserve", lambda: reserve(DB, mid)),
("My books", lambda: my_books(DB, mid))])
DB = load()
if __name__ == "__main__":
try:
run_menu("LIS", [("Librarian", librarian_menu),
("Member", member_menu)])
except EOFError:
pass
save(DB)
print("Data saved to", os.path.basename(DATA_FILE))Sample Output
Input piped to the program in this order: librarian adds three books, registers two members, issues B001 to M001 back-dated to 2026-09-01, issues B002 to M001 dated today; member M002 searches “software”, reserves B001, views her books; librarian returns B001 on 2026-09-20, tries to issue B001 to M001, issues B001 to M002, prints reports; exit. Run on 26 September 2026 with no existing data file.
LIS: 1 Librarian 2 Member 0 Back
Choice:
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Title: Author: Subject: added B001: Software Engineering
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Title: Author: Subject: added B002: Let Us C
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Title: Author: Subject: added B003: Database System Concepts
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Name: Phone: registered M001: Ravi Kumar
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Name: Phone: registered M002: Meena Joshi
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Member id: Accession no: Issue date (blank = today): issued B001 to Ravi Kumar, due 2026-09-15
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Member id: Accession no: Issue date (blank = today): issued B002 to Ravi Kumar, due 2026-10-10
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice:
LIS: 1 Librarian 2 Member 0 Back
Choice: Member id: welcome Meena Joshi
Member: 1 Search 2 Reserve 3 My books 0 Back
Choice: Search text: B001 Software Engineering Pressman issued
Member: 1 Search 2 Reserve 3 My books 0 Back
Choice: Accession no: reserved; you are number 1
Member: 1 Search 2 Reserve 3 My books 0 Back
Choice: total fines paid so far: Rs 0
Member: 1 Search 2 Reserve 3 My books 0 Back
Choice:
LIS: 1 Librarian 2 Member 0 Back
Choice:
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Accession no: Return date (blank = today): returned B001; due was 2026-09-15, fine Rs 10
reserved: keep aside for M002 (Meena Joshi)
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Member id: Accession no: held for reservation by M002
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Member id: Accession no: Issue date (blank = today): issued B001 to Meena Joshi, due 2026-10-10
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice: Books on loan:
B002 Let Us C M001 due 2026-10-10
B001 Software Engineering M002 due 2026-10-10
Fines collected: Rs 10
Members: 2, titles: 3
Librarian: 1 Add book 2 Register member 3 Issue 4 Return 5 Reports 0 Back
Choice:
LIS: 1 Librarian 2 Member 0 Back
Choice: Data saved to lis_data.jsonHand check: issued 2026-09-01, due 2026-09-15, returned 2026-09-20 is 5 days late, 5 times Rs 2 is Rs 10. A second run with one member and four books gave “member already holds 3 books” on the fourth issue, and “My books” for that member showed the back-dated book with “fine now Rs 22” (11 days late on 26 September). A phone number of “12” was rejected with “name required, phone must be 10 digits”.
Traceability
Write in lab record| Requirement | Function(s) in lis.py | How it is met |
|---|---|---|
| LIS-FR-1 | register_member | Name and 10-digit phone validated; id M001, M002, … assigned from next_member |
| LIS-FR-2 | add_book | Title required; accession number B001, B002, … from next_book |
| LIS-FR-3 | search | Case-insensitive substring match over title, author and subject; prints status |
| LIS-FR-4 | issue_book | Refuses issued or reserved-for-another books and a fourth book (MAX_BOOKS); due = issue + LOAN_DAYS |
| LIS-FR-5 | return_book, open_loan | Finds the open loan, stores return date, sets status to available |
| LIS-FR-6 | fine_for | max(0, late) * FINE_PER_DAY; also used by my_books for “fine now” |
| LIS-FR-7 | reserve, return_book, issue_book | Queue on the book; return prints “keep aside”; issue to anyone but the first in queue is refused and issue to that member removes them from the queue |
| LIS-FR-8 | reports | Loans with OVERDUE flag, sum of fines, member and title counts |
| LIS-FR-9 | my_books | Books held, due dates, fine if returned today, fines paid so far |
| LIS-NFR-1 | load, save, run_menu | JSON file next to the program, saved after every action |
| LIS-NFR-2 | whole file | Only json, os, datetime from the standard library |
| LIS-NFR-4 | every action | Each rejection prints one indented line with the reason |
| LIS-NFR-5 | ask_date | date.fromisoformat, re-asks on bad input |
Validation Record
Write in lab recordValidator: Meena Joshi (interviewee of Session 19). Place: District Public Library, library PC. Date: Thursday 24 September 2026. Test data as in the sample run above. The validator operated the member menu herself; the developer operated the librarian menu on her instruction.
| Requirement | Test performed | Verdict | Remark |
|---|---|---|---|
| LIS-FR-1 | Registered herself with name and phone; tried a 9-digit phone | Accepted | “Good that it refuses a wrong phone” |
| LIS-FR-2 | Watched three books being added and saw B001 to B003 | Accepted | Asked whether the number could be printed on a label; noted as future item |
| LIS-FR-3 | Typed “software”, “pressman”, “dbms” and “SOFT” | Accepted | “Any word works, and it shows if the book is out. This is what I asked for” |
| LIS-FR-4 | Issued B001 back-dated, B002 today; fourth issue to same member refused | Accepted | “Due date on screen is better than the stamp” |
| LIS-FR-5 | Returned B001 on 2026-09-20 | Accepted | none |
| LIS-FR-6 | Return 5 days late showed Rs 10; a return on the due date showed Rs 0 | Accepted with remark | “Correct by the library rule, but it still charges for closed days” (LIS-FR-16 is a Should) |
| LIS-FR-7 | Reserved B001 while issued; after return, issue to another member was refused and issue to her went through | Accepted | “This fixes the slip problem. Nobody can jump the queue” |
| LIS-FR-8 | Viewed reports after the run | Accepted | “The librarian will like the fine total” |
| LIS-FR-9 | Opened “My books” with two books held | Accepted with remark | Wanted the books sorted by due date; agreed as a small change for the next version |
| LIS-NFR-1 | Closed the program, reopened it, data still there | Accepted | none |
| LIS-NFR-2 | Ran on the library PC by double-clicking the file | Accepted | none |
| LIS-NFR-4 | Typed a wrong member id and a wrong accession number | Accepted | none |
| LIS-NFR-5 | Typed 20/09/2026 and was asked again for YYYY-MM-DD | Accepted with remark | “I would prefer typing the date the Indian way”; kept as is to avoid day-month confusion, explained to the validator |
Result: 13 items tested, 10 Accepted, 3 Accepted with remark, 0 Rejected. Exit rule of the Session 19 validation plan is met. Signed: Meena Joshi, 24 September 2026.
Should Items Left Out
Write in lab record| Id | Requirement | Why left out | Effort to add |
|---|---|---|---|
| LIS-FR-10 | Renew once if no reservation | Not a Must; the counter can re-issue | One function: check reserved_by empty, extend due by 14 days |
| LIS-FR-11 | Edit member, close membership | Not a Must; register is small | One menu item editing the member dictionary |
| LIS-FR-13 | Record fine payment and waiver separately | Library collects at the counter on return; fine is stored on the loan | Add paid flag on loan, a menu item to mark paid or waived |
| LIS-FR-16 | Skip closed days in fine | Library’s stated rule today is calendar days | A list of closed dates and a loop in fine_for |
| LIS-NFR-3 | 2 seconds on 5,000 titles | Not measured; linear scans on a dictionary of that size finish well within it | Measure with a generated file before promising |
| LIS-FR-12, LIS-FR-14 | Multiple copies, ISBN, popular titles report | Could-have | Copy count on the book, group loans by title |
| LIS-FR-15 | SMS reminder | Won’t have: no internet on the library PC | Needs a gateway; out of scope |
Viva Questions
Do not copy. Read for understanding and the viva- Q: How does the program satisfy LIS-FR-6? A:
fine_forreturnsmax(0, return minus due) * 2, where due is issue plus 14 days. - Q: Why is a loan a separate record and not a field on the book? A: A book has many loans over time and the fine report needs all of them.
- Q: How is a reservation enforced? A:
issue_bookrefuses any member other than the first inreserved_by; issuing to that member pops the queue. - Q: Why save after every action? A: LIS-NFR-1; a crash loses at most the current action.
- Q: Why can the librarian type an issue date? A: To back-date an issue during validation so that the fine rule can be demonstrated today.
- Q: What is the difference between the sample run and the validation record? A: The sample run is the developer verifying output; the validation record is the user judging whether the requirement was met.
- Q: Which requirements were not built and why? A: LIS-FR-10, 11, 13, 16 and NFR-3 are Should items left for the next version; FR-15 needs SMS, which the library cannot use.
- Q: What would change if a title had many copies? A: LIS-FR-12: books would need a copy count, and status would move from the title to each copy.
Common Mistakes
Do not copy. Read for understanding and the viva- Building features the requirements list does not contain, and then having no id to trace them to.
- Storing the due date only in a print statement instead of on the loan record, so the fine cannot be computed later.
- Using floating point days or string comparison for dates; use
dateobjects. - Validation table with the developer’s own verdicts; the user must give the verdict, in their words.
- Forgetting to test the rejection paths (fourth book, wrong phone, reserved book) that the user will try first.
Session Summary
Write in lab record- Source listing of
lis.pywith docstrings naming the requirement each function implements. - Sample run with hand check of the fine amount and the three-book limit.
- Traceability table from every Must requirement to its function.
- Validation record signed by the Session 19 interviewee, with verdicts and remarks.
- Should, Could and Won’t items with the reason each was left out.