Python OOP Project: Library Book Tracker

Python OOP Project: Library Book Tracker

Welcome to another hands-on Python project! Today, we’re going to build a complete Library Book Tracker using Object-Oriented Programming (OOP) principles. By the end of this guide, you’ll have a working program to manage book checkouts, returns, and user accounts—all while practicing key OOP concepts like classes, objects, inheritance, and encapsulation.


Why Use OOP for This Project?

Before we dive into the code, let’s briefly discuss why OOP is a great fit for this type of application. In a library system, you have distinct entities—like books, users, and transactions—that have their own attributes and behaviors. OOP allows us to model these entities as classes, making our code organized, reusable, and easier to maintain.


Core Classes and Their Responsibilities

We’ll structure our application around three main classes: Book, User, and Library. Each class will encapsulate relevant data and methods.

Let’s start by defining our Book class:

class Book:
    def __init__(self, title, author, isbn):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.is_checked_out = False

    def check_out(self):
        if not self.is_checked_out:
            self.is_checked_out = True
            return True
        return False

    def return_book(self):
        if self.is_checked_out:
            self.is_checked_out = False
            return True
        return False

    def __str__(self):
        status = "Checked Out" if self.is_checked_out else "Available"
        return f"{self.title} by {self.author} [{status}]"

Next, we define the User class to represent library members:

class User:
    def __init__(self, name, user_id):
        self.name = name
        self.user_id = user_id
        self.checked_out_books = []

    def check_out_book(self, book):
        if book.check_out():
            self.checked_out_books.append(book)
            return True
        return False

    def return_book(self, book):
        if book in self.checked_out_books and book.return_book():
            self.checked_out_books.remove(book)
            return True
        return False

    def __str__(self):
        return f"User: {self.name} (ID: {self.user_id})"

Finally, the Library class will serve as the central manager for books and users:

class Library:
    def __init__(self):
        self.books = []
        self.users = []

    def add_book(self, book):
        self.books.append(book)

    def add_user(self, user):
        self.users.append(user)

    def find_book(self, isbn):
        for book in self.books:
            if book.isbn == isbn:
                return book
        return None

    def find_user(self, user_id):
        for user in self.users:
            if user.user_id == user_id:
                return user
        return None

Example Usage and Workflow

Let’s see how these classes work together. First, we’ll create a library instance and populate it with books and users:

# Initialize library
my_library = Library()

# Add some books
book1 = Book("Python Crash Course", "Eric Matthes", "9781593279288")
book2 = Book("Deep Work", "Cal Newport", "9781455586691")
my_library.add_book(book1)
my_library.add_book(book2)

# Add a user
user1 = User("Alice", "U001")
my_library.add_user(user1)

Now, let’s simulate a checkout:

# Alice checks out a book
user1.check_out_book(book1)
print(book1)  # Output: Python Crash Course by Eric Matthes [Checked Out]

And a return:

# Alice returns the book
user1.return_book(book1)
print(book1)  # Output: Python Crash Course by Eric Matthes [Available]

Enhancing Functionality with Search and Reports

To make our library more practical, let’s add methods to search for books by title or author and generate simple reports.

We’ll extend the Library class:

class Library:
    # ... (previous code)

    def search_books(self, title=None, author=None):
        results = []
        for book in self.books:
            if title and title.lower() in book.title.lower():
                results.append(book)
            elif author and author.lower() in book.author.lower():
                results.append(book)
        return results

    def generate_report(self):
        available = sum(1 for book in self.books if not book.is_checked_out)
        checked_out = len(self.books) - available
        report = f"Total Books: {len(self.books)}\nAvailable: {available}\nChecked Out: {checked_out}"
        return report

Example usage:

# Search for books
results = my_library.search_books(author="Eric")
for book in results:
    print(book)

# Generate a report
print(my_library.generate_report())

Sample Library Data

Here’s a small dataset you can use to test the system:

Title Author ISBN
Python Crash Course Eric Matthes 9781593279288
Deep Work Cal Newport 9781455586691
Atomic Habits James Clear 9780735211292
The Pragmatic Programmer Andrew Hunt 9780201616224

Adding Error Handling and Validation

To make our application more robust, we should add error handling. For example, we should prevent a user from checking out a book that’s already checked out, or returning a book they don’t have.

Let’s improve the check_out_book and return_book methods in the User class:

class User:
    # ... (previous code)

    def check_out_book(self, book):
        if book in self.checked_out_books:
            print("You already have this book checked out.")
            return False
        if book.is_checked_out:
            print("This book is already checked out by someone else.")
            return False
        if book.check_out():
            self.checked_out_books.append(book)
            print(f"Successfully checked out: {book.title}")
            return True
        return False

    def return_book(self, book):
        if book not in self.checked_out_books:
            print("You don't have this book checked out.")
            return False
        if book.return_book():
            self.checked_out_books.remove(book)
            print(f"Successfully returned: {book.title}")
            return True
        return False

Now, let’s test these improvements:

# Try to check out the same book twice
user1.check_out_book(book1)  # Success
user1.check_out_book(book1)  # Error message

# Try to return a book not checked out
user1.return_book(book2)     # Error message

Storing Data with JSON

To make our library data persistent, we can add methods to save and load books and users to/from a JSON file.

First, let’s add a to_dict() method to our Book and User classes:

class Book:
    # ... (previous code)

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

class User:
    # ... (previous code)

    def to_dict(self):
        return {
            "name": self.name,
            "user_id": self.user_id,
            "checked_out_books": [book.isbn for book in self.checked_out_books]
        }

Then, in the Library class, we add methods to save and load data:

import json

class Library:
    # ... (previous code)

    def save_data(self, filename="library_data.json"):
        data = {
            "books": [book.to_dict() for book in self.books],
            "users": [user.to_dict() for user in self.users]
        }
        with open(filename, 'w') as f:
            json.dump(data, f, indent=4)

    def load_data(self, filename="library_data.json"):
        try:
            with open(filename, 'r') as f:
                data = json.load(f)
            # Reconstruct books and users from dictionaries
            self.books = []
            for book_data in data["books"]:
                book = Book(book_data["title"], book_data["author"], book_data["isbn"])
                book.is_checked_out = book_data["is_checked_out"]
                self.books.append(book)

            self.users = []
            for user_data in data["users"]:
                user = User(user_data["name"], user_data["user_id"])
                # Reattach checked-out books by ISBN
                for isbn in user_data["checked_out_books"]:
                    book = self.find_book(isbn)
                    if book:
                        user.checked_out_books.append(book)
                self.users.append(user)
        except FileNotFoundError:
            print("No saved data found.")

Example usage:

# Save the current state
my_library.save_data()

# Later, load it back
my_library = Library()
my_library.load_data()

Testing the Application

Let’s write a simple interactive script to test our library system:

def main():
    library = Library()
    library.load_data()  # Load previous data if exists

    while True:
        print("\n--- Library Menu ---")
        print("1. Add Book")
        print("2. Add User")
        print("3. Check Out Book")
        print("4. Return Book")
        print("5. Search Books")
        print("6. Generate Report")
        print("7. Save and Exit")

        choice = input("Enter your choice: ")

        if choice == "1":
            title = input("Enter book title: ")
            author = input("Enter author: ")
            isbn = input("Enter ISBN: ")
            book = Book(title, author, isbn)
            library.add_book(book)
            print("Book added successfully!")

        elif choice == "2":
            name = input("Enter user name: ")
            user_id = input("Enter user ID: ")
            user = User(name, user_id)
            library.add_user(user)
            print("User added successfully!")

        elif choice == "3":
            user_id = input("Enter your user ID: ")
            isbn = input("Enter book ISBN to check out: ")
            user = library.find_user(user_id)
            book = library.find_book(isbn)
            if user and book:
                user.check_out_book(book)
            else:
                print("User or book not found.")

        elif choice == "4":
            user_id = input("Enter your user ID: ")
            isbn = input("Enter book ISBN to return: ")
            user = library.find_user(user_id)
            book = library.find_book(isbn)
            if user and book:
                user.return_book(book)
            else:
                print("User or book not found.")

        elif choice == "5":
            title = input("Enter title (or leave blank): ")
            author = input("Enter author (or leave blank): ")
            results = library.search_books(title, author)
            for book in results:
                print(book)

        elif choice == "6":
            print(library.generate_report())

        elif choice == "7":
            library.save_data()
            print("Data saved. Goodbye!")
            break

        else:
            print("Invalid choice. Try again.")

if __name__ == "__main__":
    main()

Key Takeaways and Best Practices

  • Encapsulation: Each class manages its own data and behavior.
  • Reusability: Classes can be easily extended or reused in other projects.
  • Maintainability: Code is organized and easier to debug or enhance.

By building this project, you’ve practiced:

  • Defining classes and creating objects.
  • Implementing class methods and attributes.
  • Using dunder methods like __str__.
  • Handling JSON for data persistence.
  • Building an interactive console application.

Final Thoughts and Next Steps

You now have a fully functional Library Book Tracker! Feel free to expand it further—perhaps by adding due dates, late fees, or a graphical interface with Tkinter or PyQt.

Remember, the best way to learn OOP is by building projects like this. Try modifying the code, adding new features, or even starting from scratch to reinforce your understanding.

Happy coding