Skip to content

Session 20

Library Information System in Python with JSON persistence, sample run, traceability and user validation record

Updated View as Markdown

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 record

Sessions 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 viva

One 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
lis.pypython
"""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.json

Hand 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
RequirementFunction(s) in lis.pyHow it is met
LIS-FR-1register_memberName and 10-digit phone validated; id M001, M002, … assigned from next_member
LIS-FR-2add_bookTitle required; accession number B001, B002, … from next_book
LIS-FR-3searchCase-insensitive substring match over title, author and subject; prints status
LIS-FR-4issue_bookRefuses issued or reserved-for-another books and a fourth book (MAX_BOOKS); due = issue + LOAN_DAYS
LIS-FR-5return_book, open_loanFinds the open loan, stores return date, sets status to available
LIS-FR-6fine_formax(0, late) * FINE_PER_DAY; also used by my_books for “fine now”
LIS-FR-7reserve, return_book, issue_bookQueue 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-8reportsLoans with OVERDUE flag, sum of fines, member and title counts
LIS-FR-9my_booksBooks held, due dates, fine if returned today, fines paid so far
LIS-NFR-1load, save, run_menuJSON file next to the program, saved after every action
LIS-NFR-2whole fileOnly json, os, datetime from the standard library
LIS-NFR-4every actionEach rejection prints one indented line with the reason
LIS-NFR-5ask_datedate.fromisoformat, re-asks on bad input

Validation Record

Write in lab record

Validator: 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.

RequirementTest performedVerdictRemark
LIS-FR-1Registered herself with name and phone; tried a 9-digit phoneAccepted“Good that it refuses a wrong phone”
LIS-FR-2Watched three books being added and saw B001 to B003AcceptedAsked whether the number could be printed on a label; noted as future item
LIS-FR-3Typed “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-4Issued B001 back-dated, B002 today; fourth issue to same member refusedAccepted“Due date on screen is better than the stamp”
LIS-FR-5Returned B001 on 2026-09-20Acceptednone
LIS-FR-6Return 5 days late showed Rs 10; a return on the due date showed Rs 0Accepted with remark“Correct by the library rule, but it still charges for closed days” (LIS-FR-16 is a Should)
LIS-FR-7Reserved B001 while issued; after return, issue to another member was refused and issue to her went throughAccepted“This fixes the slip problem. Nobody can jump the queue”
LIS-FR-8Viewed reports after the runAccepted“The librarian will like the fine total”
LIS-FR-9Opened “My books” with two books heldAccepted with remarkWanted the books sorted by due date; agreed as a small change for the next version
LIS-NFR-1Closed the program, reopened it, data still thereAcceptednone
LIS-NFR-2Ran on the library PC by double-clicking the fileAcceptednone
LIS-NFR-4Typed a wrong member id and a wrong accession numberAcceptednone
LIS-NFR-5Typed 20/09/2026 and was asked again for YYYY-MM-DDAccepted 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
IdRequirementWhy left outEffort to add
LIS-FR-10Renew once if no reservationNot a Must; the counter can re-issueOne function: check reserved_by empty, extend due by 14 days
LIS-FR-11Edit member, close membershipNot a Must; register is smallOne menu item editing the member dictionary
LIS-FR-13Record fine payment and waiver separatelyLibrary collects at the counter on return; fine is stored on the loanAdd paid flag on loan, a menu item to mark paid or waived
LIS-FR-16Skip closed days in fineLibrary’s stated rule today is calendar daysA list of closed dates and a loop in fine_for
LIS-NFR-32 seconds on 5,000 titlesNot measured; linear scans on a dictionary of that size finish well within itMeasure with a generated file before promising
LIS-FR-12, LIS-FR-14Multiple copies, ISBN, popular titles reportCould-haveCopy count on the book, group loans by title
LIS-FR-15SMS reminderWon’t have: no internet on the library PCNeeds 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_for returns max(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_book refuses any member other than the first in reserved_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 date objects.
  • 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.py with 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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close