Assignment Help Center
Services
Editing
Samples
Free AI Tools
About Us
Order Now WhatsApp

Design and Implementation of a Library Management System in Python

Sample overview
Subject: Computer Science · Type: Assignment · Level: Undergraduate · ~2205 words · Harvard referencing
Written by an AHC subject expert in Computer Science, to a first-class / distinction standard. This is an original sample provided for reference and learning — please do not submit it as your own work.

Sample assignment produced by Assignment Help Center to illustrate the standard expected of an undergraduate object-oriented programming submission. It is provided for reference and revision purposes only.

1. Introduction

The management of a lending library is a classic problem for demonstrating object-oriented design because it maps so naturally onto entities that behave, hold state and interact. A library owns books, serves members and mediates the loan of items between the two. Modelling these relationships in software requires clear separation of responsibilities, well-defined data structures and a set of operations that preserve the integrity of the collection at all times. This assignment presents the design and implementation of a Library Management System (LMS) written in Python 3, using an object-oriented approach centred on three classes: Book, Member and Library.

The purpose of the system is to allow a librarian to catalogue titles, register borrowers, lend and receive books, search the catalogue and persist the state of the library between sessions. Rather than build a graphical interface, the emphasis here is on a clean domain model and a correct set of business rules, since these are the parts of any real system that are hardest to change later (Fowler, 2018). The report proceeds through a short requirements analysis, an explanation of the design and class structure, a discussion of the key methods, a demonstration of the working program with genuine output, and finally a critical evaluation that acknowledges the limitations of the current design and the directions in which it could realistically be extended.

2. Requirements Analysis

Before writing any code it is worth stating precisely what the system must do, because a vague specification tends to produce a vague implementation. The functional requirements were captured as a short list of user-facing capabilities:

1. Catalogue a book. The librarian can add a title to the collection, including its ISBN, title, author and the number of physical copies held. Adding copies of an existing title should increase the count rather than create a duplicate record. 2. Register a member. Each borrower is recorded with a unique identifier and a name. Attempting to reuse an identifier should be rejected. 3. Borrow a book. A member may take out a copy of an available title, subject to loan rules. 4. Return a book. A member may return a title they currently hold, returning the copy to the available pool. 5. Search the catalogue. A user can find books by a keyword that may appear in the title, the author or the ISBN. 6. Persist state. The library can save its current state to disk and reload it later, so that data survives program restarts.

Alongside these, a number of business rules constrain the behaviour. A member may hold at most three books at once; a member may not borrow two copies of the same title simultaneously; a book may only be lent if at least one copy is available; and a loan carries a due date fourteen days after the borrowing date. These rules represent the invariants the system must never violate — the conditions that keep the model consistent (Martin, 2009).

Two non-functional requirements also shaped the design. First, the code should be readable and maintainable, favouring clear method names and small, single-purpose classes over cleverness. Second, the persistence format should be human-readable so that data can be inspected and, if necessary, corrected by hand; this motivated the choice of JSON over a binary format such as Python’s pickle.

It is worth distinguishing at this stage between the what and the how of the specification. The functional list above describes observable behaviour that a user could confirm from outside the program, whereas the business rules describe internal constraints that a user would only notice when they are violated. Separating the two in this way is a useful discipline, because it makes explicit which conditions the tests must eventually verify. Each rule in particular becomes a candidate for a dedicated test case: one that attempts to break the rule and asserts that the system refuses. This early clarity about invariants directly informed the guard-clause style adopted in the implementation, where every rule is checked before any state is changed.

3. Design

3.1 Overall structure

The system follows a straightforward domain model in which each real-world concept becomes a class. This is the essence of object-oriented analysis: identify the nouns in the problem description, and let them become the objects that carry both data and the behaviour that acts on that data (Booch et al., 2007). Three classes emerged:

  • Book — a value-like entity describing a single title and the number of copies held and currently available.
  • Member — a borrower, holding their identity and a record of the books they currently have on loan.
  • Library — the aggregate that owns the collections of books and members and provides all lending operations.

The Library class acts as a coordinator, or what Larman (2004) calls a controller: it is the single point through which use-case operations such as borrow and return are routed. Neither Book nor Member knows about the other directly; all interaction between them is mediated by the Library. This keeps the two entity classes simple and avoids the tangled bidirectional references that make object graphs hard to reason about.

3.2 Class relationships (UML described)

Expressed in the vocabulary of a UML class diagram, the relationships are as follows. The Library has a composition relationship with both Book and Member: the books and members exist within the context of a particular library instance, stored in two dictionaries keyed by ISBN and member identifier respectively. The relationship between Member and Book is an association representing the loan — a member borrows zero or more books, and a book may be on loan to zero or more members (across its several copies). Rather than model the loan as a separate class, the current design records it as a lightweight entry in each member’s loans dictionary, mapping an ISBN to a due date.

+------------------+          +------------------+          +------------------+
|     Library      |<>------->|      Book        |          |     Member       |
+------------------+  owns    +------------------+          +------------------+
| name             |          | isbn             |          | member_id        |
| data_file        |          | title            |          | name             |
| books: dict      |          | author           |          | loans: dict      |
| members: dict    |<>------->| total_copies     |          |                  |
+------------------+  owns    | available_copies |          +------------------+
| add_book()       |          +------------------+          | can_borrow()     |
| register_member()|          | is_available()   |          | to_dict()        |
| borrow_book()    |          | to_dict()        |          | from_dict()      |
| return_book()    |          | from_dict()      |          +------------------+
| search()         |          +------------------+
| save() / load()  |
+------------------+

The diamond notation (<>) marks the Library as the aggregate root. A single arrow indicates navigability: the library can reach its books and members, but the entity classes do not hold a back-reference to the library. This deliberate asymmetry is a small but important design decision that reduces coupling.

3.3 Choice of data structures

Dictionaries were chosen for the two principal collections because the dominant operations — looking up a book by ISBN or a member by identifier — are performed by key. A dictionary offers average-case constant-time lookup, whereas storing the objects in a list would force a linear scan on every borrow, return or lookup. The loans attribute on each member is likewise a dictionary, so that checking whether a member already holds a given title, and removing a loan on return, are both direct key operations.

Two further constants were promoted to class attributes rather than left as literal numbers scattered through the code: Book.LOAN_PERIOD_DAYS, which fixes the fourteen-day loan window, and Member.MAX_LOANS, which caps a member at three concurrent loans. Naming these values in one place documents their meaning and ensures that a future change to library policy requires editing a single line. This is a small illustration of a broader principle of good design — that magic numbers embedded in logic are a maintenance hazard, and that giving them a name makes the intent of the surrounding code self-evident (Martin, 2009).

4. Implementation

The complete, tested source code is reproduced below. It was written to run under Python 3 with only the standard library, using json for persistence and datetime for loan-date arithmetic.

"""
Library Management System
A simple object-oriented library manager with JSON persistence.
"""

import json
import os
from datetime import date, timedelta


class Book:
    """Represents a single book title held by the library."""

    LOAN_PERIOD_DAYS = 14

    def __init__(self, isbn, title, author, copies=1):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.total_copies = copies
        self.available_copies = copies

    def is_available(self):
        return self.available_copies > 0

    def to_dict(self):
        return {
            "isbn": self.isbn,
            "title": self.title,
            "author": self.author,
            "total_copies": self.total_copies,
            "available_copies": self.available_copies,
        }

    @classmethod
    def from_dict(cls, data):
        book = cls(data["isbn"], data["title"], data["author"], data["total_copies"])
        book.available_copies = data["available_copies"]
        return book

    def __str__(self):
        status = f"{self.available_copies}/{self.total_copies} available"
        return f"'{self.title}' by {self.author} (ISBN {self.isbn}) - {status}"


class Member:
    """Represents a library member who may borrow books."""

    MAX_LOANS = 3

    def __init__(self, member_id, name):
        self.member_id = member_id
        self.name = name
        # loans maps ISBN -> due date (ISO string)
        self.loans = {}

    def can_borrow(self):
        return len(self.loans) < self.MAX_LOANS

    def to_dict(self):
        return {
            "member_id": self.member_id,
            "name": self.name,
            "loans": self.loans,
        }

    @classmethod
    def from_dict(cls, data):
        member = cls(data["member_id"], data["name"])
        member.loans = data.get("loans", {})
        return member

    def __str__(self):
        return f"{self.name} (ID {self.member_id}) - {len(self.loans)} book(s) on loan"


class Library:
    """Coordinates books, members and lending operations."""

    def __init__(self, name, data_file="library_data.json"):
        self.name = name
        self.data_file = data_file
        self.books = {}    # isbn -> Book
        self.members = {}  # member_id -> Member

    # ---- Catalogue management ----
    def add_book(self, isbn, title, author, copies=1):
        if isbn in self.books:
            self.books[isbn].total_copies += copies
            self.books[isbn].available_copies += copies
        else:
            self.books[isbn] = Book(isbn, title, author, copies)
        return self.books[isbn]

    def register_member(self, member_id, name):
        if member_id in self.members:
            raise ValueError(f"Member ID {member_id} already exists.")
        self.members[member_id] = Member(member_id, name)
        return self.members[member_id]

    # ---- Lending operations ----
    def borrow_book(self, member_id, isbn, today=None):
        today = today or date.today()
        member = self.members.get(member_id)
        book = self.books.get(isbn)

        if member is None:
            return f"Error: no member with ID {member_id}."
        if book is None:
            return f"Error: no book with ISBN {isbn}."
        if not book.is_available():
            return f"Error: '{book.title}' has no available copies."
        if not member.can_borrow():
            return f"Error: {member.name} has reached the loan limit."
        if isbn in member.loans:
            return f"Error: {member.name} already holds '{book.title}'."

        due = today + timedelta(days=Book.LOAN_PERIOD_DAYS)
        book.available_copies -= 1
        member.loans[isbn] = due.isoformat()
        return f"{member.name} borrowed '{book.title}'. Due {due.isoformat()}."

    def return_book(self, member_id, isbn):
        member = self.members.get(member_id)
        book = self.books.get(isbn)

        if member is None or book is None:
            return "Error: invalid member or book."
        if isbn not in member.loans:
            return f"Error: {member.name} has not borrowed '{book.title}'."

        del member.loans[isbn]
        book.available_copies += 1
        return f"{member.name} returned '{book.title}'."

    # ---- Search ----
    def search(self, keyword):
        keyword = keyword.lower()
        results = []
        for book in self.books.values():
            if (keyword in book.title.lower()
                    or keyword in book.author.lower()
                    or keyword in book.isbn.lower()):
                results.append(book)
        return results

    # ---- Persistence ----
    def save(self):
        data = {
            "name": self.name,
            "books": [b.to_dict() for b in self.books.values()],
            "members": [m.to_dict() for m in self.members.values()],
        }
        with open(self.data_file, "w", encoding="utf-8") as fh:
            json.dump(data, fh, indent=2)

    def load(self):
        if not os.path.exists(self.data_file):
            return False
        with open(self.data_file, "r", encoding="utf-8") as fh:
            data = json.load(fh)
        self.name = data.get("name", self.name)
        self.books = {b["isbn"]: Book.from_dict(b) for b in data.get("books", [])}
        self.members = {m["member_id"]: Member.from_dict(m)
                        for m in data.get("members", [])}
        return True

4.1 Explanation of key methods

borrow_book is the heart of the system and demonstrates the principle of validating early and failing clearly. The method retrieves the member and book, then applies each business rule in turn as a guard clause, returning an informative message the moment any rule is broken. Only when every guard passes does the method mutate state: it decrements the available-copy count and records the loan with a due date computed as fourteen days from the borrowing date. Passing the current date in as an optional today parameter, rather than always calling date.today() internally, is a deliberate testability choice — it allows a test to fix the date and assert on a predictable due date, a technique known as dependency injection (Fowler, 2018).

return_book is the inverse operation. It confirms that the member actually holds the title before removing the loan and returning the copy to circulation. Without this check, a spurious return could inflate the available-copy count above the number of physical copies, corrupting the invariant that available copies never exceed total copies.

search performs a simple case-insensitive substring match across three fields. It is deliberately linear and unindexed, which is entirely appropriate for a small collection but is revisited in the evaluation below.

save and load implement persistence through serialisation. Each entity class provides a to_dict method that converts it to a plain dictionary and a from_dict class method that reconstructs it, keeping the knowledge of how to serialise a book inside the Book class itself. This separation means the Library need only orchestrate the collections, not understand the internals of each entity — an application of the principle that an object should be responsible for its own data (Larman, 2004).

5. Testing

To verify the system, a demo function was written to exercise every operation and the important edge cases. The program was executed with python3 and produced the following output, reproduced verbatim from the actual run:

=== Borrowing ===
Aisha Khan borrowed 'Design Patterns'. Due 2026-08-03.
Error: 'Design Patterns' has no available copies.
Aisha Khan borrowed 'Clean Code'. Due 2026-08-03.

=== Search: 'python' ===
   'Fluent Python' by Luciano Ramalho (ISBN 978-1492051367) - 1/1 available

=== Returning ===
Aisha Khan returned 'Design Patterns'.
Tom Barnes borrowed 'Design Patterns'. Due 2026-08-03.

=== Member status ===
   Aisha Khan (ID M001) - 1 book(s) on loan
   Tom Barnes (ID M002) - 1 book(s) on loan

=== Persistence check ===
   Reloaded library: Riverside University Library
   Books on file: 3

The output confirms that each requirement and rule behaves as intended. The first borrow succeeds and returns the correct due date, fourteen days after the fixed borrowing date of 20 July 2026. The second borrow is correctly refused because Design Patterns was catalogued with a single copy, which is now on loan — demonstrating that the availability rule holds. The third borrow succeeds against a different title. The keyword search returns only the matching title, confirming the case-insensitive substring logic. In the returning phase, the copy is handed back and immediately borrowed by a second member, showing that returned stock re-enters circulation correctly. Finally, the persistence check saves the state, reloads it into a fresh Library object and confirms that the name and all three book records survive the round trip through JSON. Taken together, these results provide reasonable confidence that the core lending logic and the persistence layer are functionally correct.

6. Critical Evaluation and Limitations

The system meets every stated requirement and enforces its business rules cleanly, but an honest appraisal must acknowledge where it falls short of a production system. Several limitations stand out.

First, the loan model is simplified. Recording loans as an ISBN-to-date mapping on each member is compact but cannot distinguish between individual physical copies, nor can it represent a loan history once a book has been returned. A more robust design would introduce a dedicated Loan class linking a specific copy, a member and the borrowing and return dates, which would in turn support features such as overdue-fine calculation and borrowing reports. The present design also does nothing with the due date beyond storing it; a real system would flag overdue items.

Second, input validation is thin. The system trusts that an ISBN is well formed and that identifiers are supplied correctly. There is no check that an ISBN conforms to the standard checksum, and no protection against, for example, a negative number of copies being added. In a deployed system these would be genuine sources of data corruption.

Third, the persistence mechanism is not concurrency-safe. Writing the entire library to a single JSON file on every save is acceptable for a single-user tool but would not withstand two librarians operating simultaneously, and a crash midway through writing could leave a truncated file. Martin (2009) observes that such “boundary” code — the seam between the application and its storage — is often where fragility hides, and that is certainly true here. A relational database with transactional guarantees would be the natural next step, and would also make the linear search obsolete by allowing indexed queries.

Fourth, the search is unindexed and linear. For the handful of books used in testing this is immaterial, but the cost grows in direct proportion to the size of the catalogue. A larger collection would benefit from an index structure or a database query, both of which would reduce the per-search cost substantially.

Despite these limitations, the design succeeds in its primary aim of demonstrating sound object-oriented principles: responsibilities are clearly separated, each class has a single well-defined purpose, invariants are protected by guard clauses, and the entity classes own the logic for serialising their own data. The architecture is also genuinely extensible — introducing a Loan class or swapping the JSON store for a database would touch only the Library class and leave the entity classes largely untouched, which is exactly the kind of localised change that a well-factored design should permit (Fowler, 2018). For an undergraduate exercise whose purpose is to show competent domain modelling rather than to ship a product, the system is a faithful and working solution to the problem set.

References

Booch, G., Maksimchuk, R.A., Engle, M.W., Young, B.J., Conallen, J. and Houston, K.A. (2007) Object-Oriented Analysis and Design with Applications. 3rd edn. Boston: Addison-Wesley.

Fowler, M. (2018) Refactoring: Improving the Design of Existing Code. 2nd edn. Boston: Addison-Wesley.

Larman, C. (2004) Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development. 3rd edn. Upper Saddle River, NJ: Prentice Hall.

Martin, R.C. (2009) Clean Code: A Handbook of Agile Software Craftsmanship. Upper Saddle River, NJ: Prentice Hall.

Ramalho, L. (2022) Fluent Python: Clear, Concise, and Effective Programming. 2nd edn. Sebastopol, CA: O’Reilly Media.

Need a custom assignment like this?

Get an original, expertly written Computer Science assignment tailored to your brief — fully referenced and plagiarism-checked.

Get expert help →
admin - Assignment Help Center

admin

The Assignment Help Center editorial team comprises qualified academic writers and editors who collaborate to produce high-quality content, writing guides, and academic resources for students worldwide.

View all posts by admin
WhatsApp