School Library Issue Register
Issue and return books, hold copies to account, and print the overdue list.
1Introduction: the problem it solves
A school library with two thousand books runs on a card tray and a register. Nobody can answer 'do we still have a copy of this?' without walking to the shelf, and the overdue list is compiled by reading every page of the register.
Both questions are one query each if the register lives in a database instead of a book.
A school or college library, a community reading room, or a departmental book bank.
Why it is worth computerising
A card tray works because a library is small. As the collection grows, two questions become slow at exactly the same rate: whether a copy of a title is free, and who is holding a book that is overdue. Neither can be answered without walking to the shelf or reading the register page by page, and both are asked every day.
Storing the same information in three linked tables changes the cost of asking. The copies count is kept honest by the program itself, because the same action that records an issue reduces it. The overdue list, which took an afternoon, becomes one query — and can therefore be run every morning instead of once a term.
Objectives
- To hold the books, the members and the loans in three related tables
- To refuse an issue when no copy is free, so the shelf and the register agree
- To calculate the due date from the loan period instead of writing it by hand
- To work out the fine automatically from the days a book is overdue
- To produce the overdue list as a single query rather than by reading the register
2How the job is done today
Before writing anything it is worth asking how the work is handled at present, and why each of those answers falls short. These were examined:
The traditional system, and a good one until the collection grows. Answering 'is a copy free?' means walking to the shelf, and the overdue list means reading every page.
Full library systems used by colleges. They are far larger than a school library needs and take real effort to install and maintain.
An improvement on paper. It cannot easily keep the copies count honest, because nothing links an issue back to the book record.
3Functionalities
- Three tables: the books, the members and the loans between them
- Issuing checks that a copy is actually free before writing the loan
- The copies count goes down on issue and back up on return, so the shelf and the database agree
- Due date is worked out from the loan period, not typed in
- Returns calculate the fine from the days overdue
- The overdue list is a single JOIN across all three tables
The functions that provide them
| Function | Arguments | What it does |
|---|---|---|
issue_book() | book_id, member_id | Give a book out, if a copy is free. |
return_book() | loan_id | Take a book back, and charge a fine if it is late. |
overdue_list() | — | Every book still out past its due date. |
4Technical details
| Language | Python 3 |
| Storage | MySQL, through mysql-connector-python |
| Modules used |
|
5How the data is stored
Three tables, and the relationships are the point. `loans` holds only the ids of a book and a member; the titles and names live once, in their own tables. That is why a book renamed in `books` is renamed everywhere at once.
Table: books
| Field | Type | Description |
|---|---|---|
book_id | INT (primary key, auto) | Unique number for each title, generated by MySQL |
title | VARCHAR(60) (required) | Name of the book |
author | VARCHAR(40) | Author's name |
copies | INT | How many copies are on the shelf right now |
Table: members
| Field | Type | Description |
|---|---|---|
member_id | VARCHAR(6) (primary key) | Library card number, e.g. M014 |
name | VARCHAR(40) (required) | Member's name |
class | VARCHAR(6) | Class the member studies in |
Table: loans
| Field | Type | Description |
|---|---|---|
loan_id | INT (primary key, auto) | Unique number for each issue |
book_id | INT | Which book was issued — refers to books |
member_id | VARCHAR(6) | Who took it — refers to members |
issue_date | DATE | Date the book left the library |
due_date | DATE | Issue date plus the loan period |
return_date | DATE | Date it came back; empty while still out |
fine | DECIMAL(6,2) | Fine charged on return, if any |
Sample rows in books
| title | author | copies |
|---|---|---|
Wings of Fire | A P J Abdul Kalam | 3 |
The Jungle Book | R Kipling | 2 |
Sample rows in members
M014 | Ananya Sen | XII-A |
M021 | Rohit Das | XI-B |
Sample rows in loans
| book_id | member_id | issue_date | due_date |
|---|---|---|---|
2 | M021 | 2026-08-01 | 2026-08-15 |
6How it works, step by step
issue_book() checks the copies count, refuses if none are free, then INSERTs the loan and UPDATEs the count.
date.today() + timedelta(days=14) — the library's loan period is a constant at the top of the file.
return_book() finds the due date, works out days late, stores the fine and puts the copy back.
One SELECT with two JOINs, filtered on return_date IS NULL and a due date in the past.
7Source code
# ---------------------------------------------------------------------------
# library.py
#
# Issue register for the LambdaLab Public School library, using Python with
# MySQL.
#
# Three tables carry the data: books, members, and the loans between them. A
# loan stores only the ids of a book and a member — the title and the name live
# once, in their own tables, so nothing can disagree with itself.
# ---------------------------------------------------------------------------
import mysql.connector # the Python-MySQL connection
from datetime import date, timedelta # for due dates and fines
LOAN_DAYS = 14 # how long a book may be kept
FINE_PER_DAY = 2.0 # charged for each day it is late
# The connection is opened once and used by every function below. Change the
# user and password to the ones on your own computer.
db = mysql.connector.connect(host="localhost", user="root",
passwd="school", database="lambdalab_library")
# A cursor is what actually carries an SQL statement to the server and brings
# the answer back.
cur = db.cursor()
def issue_book(book_id, member_id):
"""Give a book out, if a copy is free."""
# %s is a placeholder. The value goes in the tuple that follows, and the
# connector inserts it safely — never build SQL by joining strings.
cur.execute("SELECT title, copies FROM books WHERE book_id = %s", (book_id,))
row = cur.fetchone() # one row, or None if there is none
if row is None:
print("No such book.")
return
title, copies = row
# Checked BEFORE anything is written, so the register can never show a copy
# out that does not exist.
if copies < 1:
print("'{}' — all copies are out.".format(title))
return
due = date.today() + timedelta(days=LOAN_DAYS) # date arithmetic
cur.execute("INSERT INTO loans (book_id, member_id, issue_date, due_date)"
" VALUES (%s, %s, %s, %s)", (book_id, member_id, date.today(), due))
# The same action that records the loan reduces the count, so the shelf and
# the database cannot drift apart.
cur.execute("UPDATE books SET copies = copies - 1 WHERE book_id = %s", (book_id,))
db.commit() # nothing is saved until this runs
print("Issued '{}' to {}. Due on {}.".format(title, member_id, due))
def return_book(loan_id):
"""Take a book back, and charge a fine if it is late."""
cur.execute("SELECT book_id, due_date FROM loans WHERE loan_id = %s", (loan_id,))
book_id, due = cur.fetchone()
# Subtracting two dates gives a timedelta; .days turns it into a number.
late = (date.today() - due).days
fine = late * FINE_PER_DAY if late > 0 else 0 # no fine if not late
cur.execute("UPDATE loans SET return_date = %s, fine = %s WHERE loan_id = %s",
(date.today(), fine, loan_id))
cur.execute("UPDATE books SET copies = copies + 1 WHERE book_id = %s", (book_id,))
db.commit()
print("Returned. Fine: Rs {:.2f}".format(fine))
def overdue_list():
"""Every book still out past its due date."""
# One query across all three tables. The JOINs bring the member's name and
# the book's title alongside the loan, which holds only their ids.
# return_date IS NULL means still out; NULL cannot be tested with = .
cur.execute("SELECT l.loan_id, m.name, b.title, l.due_date"
" FROM loans l JOIN members m ON l.member_id = m.member_id"
" JOIN books b ON l.book_id = b.book_id"
" WHERE l.return_date IS NULL AND l.due_date < %s"
" ORDER BY l.due_date", (date.today(),))
print("{:<8}{:<16}{:<26}{:<12}".format("LOAN", "MEMBER", "TITLE", "DUE"))
for loan_id, name, title, due in cur.fetchall(): # every matching row
print("{:<8}{:<16}{:<26}{:<12}".format(loan_id, name, title, str(due)))
# --- the program itself ----------------------------------------------------
issue_book(1, "M014") # give Wings of Fire to member M014
overdue_list() # then show what is overdue today
db.close() # always close the connection when finishedCREATE DATABASE IF NOT EXISTS lambdalab_library;
USE lambdalab_library;
CREATE TABLE books (
book_id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(60) NOT NULL,
author VARCHAR(40),
copies INT DEFAULT 1
);
CREATE TABLE members (
member_id VARCHAR(6) PRIMARY KEY,
name VARCHAR(40) NOT NULL,
class VARCHAR(6)
);
CREATE TABLE loans (
loan_id INT PRIMARY KEY AUTO_INCREMENT,
book_id INT,
member_id VARCHAR(6),
issue_date DATE,
due_date DATE,
return_date DATE DEFAULT NULL,
fine DECIMAL(6,2) DEFAULT NULL,
FOREIGN KEY (book_id) REFERENCES books(book_id),
FOREIGN KEY (member_id) REFERENCES members(member_id)
);
INSERT INTO books (title, author, copies) VALUES
('Wings of Fire', 'A P J Abdul Kalam', 3),
('The Jungle Book', 'R Kipling', 2);
INSERT INTO members VALUES
('M014', 'Ananya Sen', 'XII-A'),
('M021', 'Rohit Das', 'XI-B');
INSERT INTO loans (book_id, member_id, issue_date, due_date) VALUES
(2, 'M021', '2026-08-01', '2026-08-15');The full report as a PDF — cover page, certificate, index, code, output, future scope and references, ready to print and fill in. Or the code on its own as a zip.
8Sample output
This is a real run, not a mock-up — the transcript below is what the program actually printed.
C:\LambdaLab\Projects\Library> python library.py
Issued 'Wings of Fire' to M014. Due on 2026-09-09.
LOAN MEMBER TITLE DUE
1 Rohit Das The Jungle Book 2026-08-15schema.sql into MySQL and change the user and password in the connect() call to your own. The output above is what the queries return against the sample rows in that schema.9Testing
Every case below was actually run and the result recorded as it appeared — including the ones expected to fail.
| Test case | Expected | Actual |
|---|---|---|
| Overdue list on a date after a due date | One loan listed | 1 overdue loan(s) |
| Overdue list on a date before any due date | None listed | 0 overdue loan(s) |
| Issuing when the copies count is zero | Refused; count stays 0 | copies = 0 |
| Joining loans to members and books | Member name and title returned | Rohit Das / The Jungle Book |
| A member id that does not exist | No rows, no error | 0 row(s) |
Two kinds of case are in there on purpose. The boundary cases test the edge of a rule, where a program is most often wrong by one. The failure cases check that it refuses cleanly and says why, instead of quietly producing a wrong answer.
10Advantages
Set against the ways the job is done today:
- The copies count cannot drift from the shelf, because one action changes both
- The overdue list takes a second, so it can be produced daily
- A title's details are stored once and cannot disagree with themselves
- Fines are calculated from dates rather than estimated
- The register cannot be lost, torn or written in twice
11Future scope
What this version does not do
A report should be honest about its own limits, and each of these is the reason for one of the additions below:
- One copy is not distinguished from another, so a damaged copy cannot be traced
- There is no reservation facility for a title that is out
- Fines are calculated but not recorded as paid or waived
- Membership has no expiry, so a student who has left can still borrow
Proposed enhancements
This is also where you make the project yours. Pick one or two of these, or something nobody here thought of:
- A membership card with a barcode, scanned at the counter
- Email a reminder two days before a book is due
- Reserve a title that is currently out
- A most-borrowed report, so the librarian knows what to buy more of
- A fine waiver for a genuine reason, recorded with who approved it
12What you may have to teach yourself
CBSE expects some self-learning in a project, and says so. For this one, that means:
- Foreign keys, and why the loans table stores ids rather than names
- How to install mysql-connector-python and connect from Python
- JOIN across three tables, which is the query the whole project turns on
13Conclusion
Three tables replaced a card tray and a register, and the two questions the library could never answer quickly — is a copy free, and who is overdue — became a lookup and a single query.
The part worth keeping from this project is the discipline of storing each fact once. The loans table holds ids and nothing else, and every problem of two records disagreeing disappears with that decision.
14References
Every report needs a bibliography. This one used:
- Computer Science with Python, Class XII — the NCERT / CBSE prescribed textbook, for the chapters on database concepts, SQL and interfacing Python with a database
- Computer Science with Python, Class XI — for functions, lists, dictionaries and string handling
- Python 3 documentation — https://docs.python.org/3/
- MySQL 8.0 Reference Manual and the Connector/Python Developer Guide — https://dev.mysql.com/doc/
- CBSE Senior School Curriculum, Computer Science (Subject Code 083) — the project guidelines this report follows
- LambdaLab — https://www.lambdalab.in