DEV Community

Cover image for Building a Simple Personal Library with Python: My Experience from Zero to Execution
Shahrouz Nikseresht
Shahrouz Nikseresht

Posted on

Building a Simple Personal Library with Python: My Experience from Zero to Execution

Hey there, fellow coders and productivity hackers! In this article, I’m pulling back the curtain on Personal Library Manager, a clean, practical command-line tool I built in pure Python to keep track of every book I’ve read or want to read, no bloated apps, no spreadsheets, just a lightweight CLI that actually gets used daily.

This project is ideal for intermediate Pythonistas who want to master modular design, JSON persistence, input validation, and polished terminal UX, all without a single external dependency. I’ll walk you through my full journey: from the initial idea, through clean architecture, real-world debugging, and those “aha!” moments that turned a weekend script into a tool I open every single day.

My goal? To show you how to build real, useful software, not just another tutorial toy. Whether you’re organizing books, movies, or your own ideas, you’ll walk away ready to fork this and make it yours.

Let’s fire up the terminal and build something that sticks. 📚


📚 What is Personal Library Manager?

Personal Library Manager is a sleek, no-fuss command-line tool built in pure Python to track books you've read or want to read, ditching bloated apps and spreadsheets for a lightweight CLI that sticks around for daily use. It’s perfect for coders wanting to nail modular architecture, persistent storage, and buttery-smooth terminal UX with zero dependencies. Here’s what powers it:

  • Effortless Book Management: Add books with title, author, and year, duplicate detection keeps your list clean.
  • Instant Search & List: Partial title matching (case-insensitive) and beautifully formatted lists with numbered entries, authors, and years.
  • Edit & Delete with Smarts: Update any field or remove books safely, handles multiples gracefully with warnings.
  • Persistent JSON Storage: Data auto-saves to a readable library_data.json file, close, reopen, everything's there (UTF-8 for Persian/any language).
  • Polished Terminal UI: Custom headers, indented displays, success/warning messages, and cancel anywhere to bail out gracefully.
  • Quick Insights: One-tap summary of total books and unique authors.

Want to see it in action? Check out the demo video on YouTube. The full source code is available in the GitHub repository.


📂 Project Structure

I organized it this way so that if I want to add something later, I don't get confused:

library-project/
├── main.py              # Program starting point
├── cli.py               # Main menu and user input
├── src/
│   ├── library_core.py  # Core logic (add, delete, search, etc.)
│   └── search_module.py # Simple search
├── ui/
│   └── cli_display.py   # Nice display in the terminal
├── storage/
│   └── file_storage.py  # JSON saving and loading
└── json/
    └── library_data.json # Stored data
Enter fullscreen mode Exit fullscreen mode

⚙️ How Does It Work? (Step by Step)

1. Saving and Loading Data

The first thing I needed was a place to store the data. I didn't want the book list to disappear every time I closed the program. So I had to find a way to persist the information. I decided to use a JSON file, because it's simple, reading and writing it in Python is just two lines of code, Python's dictionary structure converts exactly to JSON, I can open it with any editor and see what's inside, and most importantly, Persian and special characters are displayed correctly, with a small setting that I'll mention later.

This part is implemented in the file storage/file_storage.py. Let's see line by line what it does.

Full Code for FileStorage

import json
import os

class FileStorage:
    def __init__(self, filepath="json/library_data.json"):
        self.filepath = filepath
Enter fullscreen mode Exit fullscreen mode

Here, I first imported two modules: json for converting Python data to JSON format and vice versa, and os for checking if the file exists. In __init__, I just store the file path, with the default being the json folder and library_data.json file. This way, if I want to change the path later, I only need to change it in one place.

Method save_data(books)

def save_data(self, books):
    """Save the list of books to a JSON file"""
    try:
        with open(self.filepath, "w", encoding="utf-8") as f:
            json.dump(books, f, indent=4, ensure_ascii=False)
        print("Data saved successfully.")
    except Exception as e:
        print(f"Error saving: {e}")
Enter fullscreen mode Exit fullscreen mode

This method takes the list of books and writes it to the file. First, with with open(..., "w", encoding="utf-8"), I open the file in write mode. The "w" mode means if the file doesn't exist, it creates it, and if it does, it clears the previous contents and writes anew. encoding="utf-8" is very important! Without it, Persian characters get saved as weird codes like \u0634\u0627\u0632\u062f\u0647 which are not readable at all.

Then, with json.dump(books, f, indent=4, ensure_ascii=False), I convert the data to JSON. indent=4 makes the output neat and spaced, like clean code, so if I want to manually open the file later, I can read it easily. ensure_ascii=False allows non-English characters (like Persian, Arabic, Chinese) to be saved exactly as they are, not as codes.

If everything goes well, it prints a success message. But if there's a problem (e.g., no access to the folder, or disk full), in except, it catches the error and prints it, but the program doesn't crash. This way, the user knows what happened, but the program continues.

Method load_data()

def load_data(self):
    """Load books from JSON file (if it exists)"""
    if not os.path.exists(self.filepath):
        print("No data file, starting from scratch.")
        return []

    try:
        with open(self.filepath, "r", encoding="utf-8") as f:
            books = json.load(f)
        print(f"{len(books)} books loaded.")
        return books
    except Exception as e:
        print(f"Error loading: {e}")
        return []
Enter fullscreen mode Exit fullscreen mode

This method is the opposite of the previous one. First, with os.path.exists(), I check if the file exists. If it's the first time running the program, there's no file, so I give a message and return an empty list, meaning the library starts from zero.

If the file exists, with with open(..., "r", encoding="utf-8"), I open it in read mode. Then with json.load(f), I read the JSON content and convert it to a list of Python dictionaries. For example, if there are two books in the file, it returns a list with two dictionaries.

After loading, I print the number of books so the user knows what was loaded. If an error occurs (e.g., the file is corrupted, or manually edited and not valid JSON), in except, it catches the error and instead of crashing, returns an empty list. This way, the program always stays alive.

Real Example from library_data.json File

After adding two books, the file looks like this:

[
    {
        "title": "1984",
        "author": "George Orwell",
        "year": 1949
    },
    {
        "title": "The Little Prince",
        "author": "Antoine de Saint-Exupéry",
        "year": 1943
    }
]
Enter fullscreen mode Exit fullscreen mode

Exactly what we have in Python! Just saved as a file. You can open it with Notepad, VS Code, or any editor and see what's inside. You can even manually add a book (but it has to be valid JSON!).

How Is It Used in the Program?

In the Library class, these two methods are called like this:

def __init__(self):
    self.storage = FileStorage()
    self.books = self.storage.load_data()  # Data comes from the file

def save(self):
    self.storage.save_data(self.books)     # Every change → gets saved
Enter fullscreen mode Exit fullscreen mode

Meaning, when the program starts, data is loaded from the file and kept in memory (variable self.books). Whenever you add, delete, or edit a book, changes are applied in memory and then with calling save(), it's immediately saved to the file. This way, no changes are lost.

Why Did I Choose This Method?

The reasons were:

First, it's persistent, even if the system restarts or you come back a month later, your books are still there. Second, it's very simple, just two small methods and everything works. Third, it's portable, you can copy the JSON file and put it on another laptop, everything transfers. Fourth, it's readable, you can see with your eyes what's inside and manually edit if you want. And fifth, it's extensible, later you can add new fields like genre, reading status, or notes, and the current code doesn't need any changes.

In summary:

This part is the beating heart of the program. Without storage, you'd have to enter books from scratch every time. With these two simple methods, your program turns from a temporary script into a real and practical tool that you can use every day.

Later, we can add automatic backup, file encryption, or even use a lightweight database like SQLite, but for a personal project, JSON is sufficient, clean, and works great.


2. Program Core: The Library Class

Here we enter the brain of the program. All the main tasks, like adding, deleting, searching, and editing books, are gathered in a clean class called Library. I put this class in src/library_core.py, because it's the core logic of the program and shouldn't mix with display or storage.

My goal was to have a place that manages everything, without worrying about storage or display details. Just say "add book", and it knows what to do.

Let's see line by line how it works.

Full Code (So Far)

# src/library_core.py
from src.search_module import SearchEngine
from storage.file_storage import FileStorage

class Library:
    def __init__(self):
        self.storage = FileStorage()
        self.books = self.storage.load_data()
Enter fullscreen mode Exit fullscreen mode

I first imported two modules: FileStorage for saving and loading, and SearchEngine for search (which I'll explain later). In __init__, I do two important things:

First, I create an object from FileStorage and keep it in self.storage. This way, whenever I want to save data later, I just call self.storage.save_data().

Second, I load the data from the file and put it in self.books. This variable is the main memory of the program. All operations (add, delete, search) are done on this list, and any change made, with a call to save(), is also saved to the file.

Method add_book(title, author, year)

def add_book(self, title, author, year):
    for book in self.books:
        if book['title'].lower() == title.lower() and book['author'].lower() == author.lower():
            return {"success": False, "message": "This book has already been added!"}

    self.books.append({"title": title, "author": author, "year": year})
    self.save()
    return {"success": True, "message": f"Book '{title}' added."}
Enter fullscreen mode Exit fullscreen mode

This method is the beating heart of adding a book. It takes three inputs: title, author, and year. But before adding anything, it does a duplicate check.

I didn't want the user to add the same book twice (e.g., "1984" by "George Orwell" twice). So in a for loop, I check all existing books. If both the title and author match, I return an error message. I used .lower() to make it case-insensitive. For example, "1984" and "nineteen eighty-four" are different, but "1984" and "1984" with uppercase don't differ.

If the book isn't duplicate, I create a new dictionary with keys title, author, and year, and add it to the list self.books with .append().

Then immediately call self.save(). This method just runs self.storage.save_data(self.books), meaning it saves the changes to the JSON file too. This way, if the program crashes or the user closes it, nothing is lost.

Finally, I return a dictionary with two keys: success (true or false) and message (message for the user). I used this format for all methods, because in the UI (display) section, it's very easy to work with. For example, if success is true, show success message, otherwise warning.

Method save() (Helper)

def save(self):
    self.storage.save_data(self.books)
Enter fullscreen mode Exit fullscreen mode

This method is very small, but you have to call it wherever there's a change. I always call it after changing the list in add_book, remove_book, update_book, etc. This way, I'm sure the data is always synced with the file.

Method list_books()

def list_books(self):
    if not self.books:
        return {"success": False, "message": "You haven't added any books yet."}
    return {"success": True, "books": self.books}
Enter fullscreen mode Exit fullscreen mode

Simple: if the list is empty, it says the library is empty. Otherwise, returns the whole list. In the UI, I take this list and display it nicely.

Method search_book(title)

def search_book(self, title):
    results = SearchEngine.find_by_title(self.books, title)
    if results:
        return {"success": True, "results": results}
    return {"success": False, "message": f"No book with title '{title}' found."}
Enter fullscreen mode Exit fullscreen mode

Here I used the SearchEngine module (which I wrote separately). It just takes the title and does a case-insensitive search (meaning "1984" and "nineteen" are found too). If something is found, it returns the list of results, otherwise error message.

Method remove_book(title)

def remove_book(self, title):
    results = SearchEngine.find_by_title(self.books, title)
    if not results:
        return {"success": False, "message": f"No book with title '{title}' found."}

    if len(results) > 1:
        return {"success": False, "message": "Multiple similar books found. Deletion requires selection."}

    book = results[0]
    self.books.remove(book)
    self.save()
    return {"success": True, "message": f"Book '{book['title']}' deleted."}
Enter fullscreen mode Exit fullscreen mode

This method is a bit smarter. First it searches. If nothing found, error. If multiple similar books found (e.g., two "Harry Potter" from different authors), it doesn't allow deletion and says you need to select more precisely (later we can add a selection menu).

If only one, it deletes with self.books.remove(book), calls save(), and gives success message.

Method update_book(...)

def update_book(self, title, new_title=None, new_author=None, new_year=None):
    results = SearchEngine.find_by_title(self.books, title)
    if not results:
        return {"success": False, "message": f"No book with title '{title}' found."}

    book = results[0]
    if new_title: book['title'] = new_title
    if new_author: book['author'] = new_author
    if new_year: book['year'] = new_year

    self.save()
    return {"success": True, "message": f"Book '{book['title']}' updated."}
Enter fullscreen mode Exit fullscreen mode

This method is for editing. It finds the book whose title matches (first one), and only changes the fields that have values. For example, you can just change the year. After change, it calls save().

Method show_summary()

def show_summary(self):
    if not self.books:
        return {"success": False, "message": "The library is empty!"}
    total = len(self.books)
    authors = len(set(b['author'] for b in self.books))
    return {"success": True, "total": total, "authors": authors}
Enter fullscreen mode Exit fullscreen mode

A quick summary: total number of books and number of unique authors. I used set to remove duplicate authors.

Why Did I Choose This Structure?

The reasons were:

First, it's clean. All logic is in one place and doesn't interfere with other parts (storage, display). Second, it's reliable. Any change made is immediately saved. Third, errors are handled. The program never crashes, always returns a clear message. Fourth, it's extensible. I can add new methods like mark_as_read() or add_genre() later without messing up the rest of the code.

My Real Experience

The first time I tested add_book, I added "1984" twice. The second time, I saw the "already added" message and got excited! Then I went to delete, I had added a wrong book, deleted it, and opened the JSON file, it was really gone! Everything automatic and hassle-free.

In summary, the Library class is like the brain of a digital librarian. It takes all commands, checks, executes, and makes sure nothing is lost. Without this class, the program was just a simple script, but with it, it's become a real tool.


3. Search: A Small Module, But Clean and Independent

One of the things that was important to me from the start was separation of tasks. I didn't want the search logic to mix into the Library class, because if I want to add more advanced search later (e.g., by author, year, genre, or even reading status), the code would get messy and tangled. So I created a separate module called search_module.py whose only job is search.

This way:

  • The Library code stays clean
  • If I want to change the search, I only touch one file
  • Later I can have multiple search types (e.g., find_by_author, find_by_year)

Let's see how it works.

Full Code for search_module.py

# src/search_module.py
class SearchEngine:
    @staticmethod
    def find_by_title(books, title):
        return [book for book in books if title.lower() in book['title'].lower()]
Enter fullscreen mode Exit fullscreen mode

That's it! Just a class with a static method. Why static? Because we don't need to create an object. We can call it directly:

SearchEngine.find_by_title(my_books, "1984")
Enter fullscreen mode Exit fullscreen mode

Without writing engine = SearchEngine().

How Does find_by_title(books, title) Work?

This method takes two inputs:

  • books: list of books (same as self.books from Library class)
  • title: string the user entered (e.g., "The Little" or "1984")

And returns a list of matching books.

Inside it, there's a List Comprehension:

[book for book in books if title.lower() in book['title'].lower()]
Enter fullscreen mode Exit fullscreen mode

Let's break it line by line:

  1. book for book in books → examines all books one by one
  2. if title.lower() in book['title'].lower() → main condition:
    • title.lower() → converts what the user entered to lowercase
    • book['title'].lower() → converts the book title to lowercase too
    • in → checks if the search string is inside the book title

For example:

  • User types: "The Little"
  • Book has: "The Little Prince"
  • "the little".lower()"the little"
  • "the little prince".lower()"the little prince"
  • "the little" in "the little prince"True → book returns

Why Did I Use in?

Because I want partial match search. Meaning:

  • "1984" → any book with "1984" in the title (e.g., "Nineteen Eighty-Four 1984 Edition")
  • "Harry" → all "Harry Potter" books
  • "Marquez" → "One Hundred Years of Solitude", "Love in the Time of Cholera", etc.

This is much more practical for a personal tool than exact search.

How Is It Used in Library?

In the Library class, it's called like this:

def search_book(self, title):
    results = SearchEngine.find_by_title(self.books, title)
    if results:
        return {"success": True, "results": results}
    return {"success": False, "message": f"No book with title '{title}' found."}
Enter fullscreen mode Exit fullscreen mode

Meaning:

  1. Delegates the search to SearchEngine
  2. Gets the result (list of books)
  3. If empty → error message
  4. If something → returns the list

The same pattern is used in remove_book and update_book.

Real Example

Suppose you have these books:

[
    {"title": "The Little Prince", "author": "Antoine de Saint-Exupéry", "year": 1943},
    {"title": "1984", "author": "George Orwell", "year": 1949},
    {"title": "One Hundred Years of Solitude", "author": "Gabriel García Márquez", "year": 1967}
]
Enter fullscreen mode Exit fullscreen mode

Now search:

Search Result
"Little" Only "The Little Prince"
"1984" Only "1984"
"a" All books! (since "a" is in all titles)
"Marquez" "One Hundred Years of Solitude"

Note: If you type "a", everything comes! This is natural behavior, but later we can add minimum search length (e.g., 2 characters).

Why Is This Separate Module Valuable?

Even if it's just one line of code now, the reasons are:

  1. Separation of Concerns

    Library just manages, SearchEngine just searches.

  2. Testable Separately

    I can write a test that only tries SearchEngine, without needing Library.

  3. Extensible

    Later I can add these:

   @staticmethod
   def find_by_author(books, author):
       ...

   @staticmethod
   def advanced_search(books, query):
       # Combined search: title + author + year
Enter fullscreen mode Exit fullscreen mode
  1. Code Readability When you write SearchEngine.find_by_title(...) in Library, it's completely clear what it's doing.

My Experience

At first, I thought "one line of code, why separate?" But when I went to delete and edit, I saw how good it is that search has a specific place. Later when I want to add advanced search, I just touch this file. Even now, if I want to remove case sensitivity or add year filter, it only takes 5 minutes.

In summary:

One line of code, but a professional module.

This SearchEngine is like a specialist search employee in the library, only knows its own job, but without it, nothing is found.


4. Terminal Display: I Made It a Bit Nice, Because It Was Important!

So far we had logic, storage, and search, but if the output is just raw and soulless text, it's like working with an old calculator. I wanted the user, when running the program, to feel like working with a real tool, not a student script. So I created a separate module called ui/cli_display.py whose only responsibility is to display information nicely in the terminal.

Why separate? Because:

  • Program logic shouldn't mix with "how to show"
  • If later I want a graphical version (like tkinter) or web, I just change this file
  • I can add different themes later (dark, light, minimal)

Let's see how it works.

Full Code for Display

# ui/cli_display.py
class Display:
    @staticmethod
    def header(title):
        line = "" * (len(title) + 10)
        print(f"\n {line}")
        print(f"   {title}")
        print(f" {line}")

    @staticmethod
    def list_books(books):
        if not books:
            print("You don't have any books yet.")
            return
        Display.header("My Books")
        for i, book in enumerate(books, 1):
            print(f"#{i}  {book['title']}")
            print(f"     Author: {book['author']}")
            print(f"     Year: {book['year']}\n")
Enter fullscreen mode Exit fullscreen mode

All methods are static, because we don't need to create an object. We can write directly Display.header("Title").

Method header(title), A Nice Title with Lines

def header(title):
    line = "" * (len(title) + 10)
    print(f"\n {line}")
    print(f"   {title}")
    print(f" {line}")
Enter fullscreen mode Exit fullscreen mode

This method creates a text box. For example, if the title is "My Books":

 ────────────────────────
    My Books
 ────────────────────────
Enter fullscreen mode Exit fullscreen mode

Let's see line by line:

  • len(title) + 10 → title length + 10 extra characters (5 on each side)
  • "─" * ... → creates a horizontal line from the character (long dash)
  • \n → puts an empty line before to separate from previous text
  • {title} → puts the title with 3 spaces from left to look centered

Note: I used (U+2500), not regular -. This character displays continuously and nicely in modern terminals.

Method list_books(books), Book List with Numbers and Details

def list_books(books):
    if not books:
        print("You don't have any books yet.")
        return
    Display.header("My Books")
    for i, book in enumerate(books, 1):
        print(f"#{i}  {book['title']}")
        print(f"     Author: {book['author']}")
        print(f"     Year: {book['year']}\n")
Enter fullscreen mode Exit fullscreen mode

This method takes the book list and displays it nicely and orderly.

Line by Line:
  1. if not books: → if list empty, says a simple message and done
  2. Display.header("My Books") → first puts a nice title
  3. enumerate(books, 1) → numbering starts from 1 (not 0)
  4. Each book has three lines:
    • #{i} {title} → number + title
    • Author: {author} → with 5 spaces indentation
    • Year: {year} → aligned with author

Real Output in Terminal

Suppose you have two books:

 ────────────────────────
    My Books
 ────────────────────────
#1  1984
     Author: George Orwell
     Year: 1949

#2  The Little Prince
     Author: Antoine de Saint-Exupéry
     Year: 1943

Enter fullscreen mode Exit fullscreen mode

Super readable and professional looking! Like working with a real app.

Other Methods I Added

@staticmethod
def search_results(matches):
    if not matches:
        print("No book found.")
        return
    Display.header("Search Results")
    for book in matches:
        print(f"{book['title']} - {book['author']} ({book['year']})")

@staticmethod
def summary(total, authors):
    Display.header("Library Summary")
    print(f"Total books: {total}")
    print(f"Unique authors: {authors}")

@staticmethod
def success(message):
    print(f"{message}")

@staticmethod
def warning(message):
    print(f"{message}")

@staticmethod
def info(message):
    print(f"{message}")
Enter fullscreen mode Exit fullscreen mode

These are for success messages, warnings, info, and search results. All use header to be consistent and professional.

How Is It Used in cli.py?

result = self.library.list_books()
if result["success"]:
    Display.list_books(result["books"])
else:
    Display.warning(result["message"])
Enter fullscreen mode Exit fullscreen mode

Meaning:

  • Library just returns data and status
  • Display just decides how to show

My Real Experience

At first, I was just doing print(book). After adding header, suddenly I felt the program came alive. When I saw the book list with those lines and numbers, I said: "Yeah, this is it!"

Then I went to search_results, when I typed "Little" and just one line came with a nice title, it really felt good. Like working with a commercial app, not a personal project.

Why Is This Section Important?

Because user experience (UX) matters, even in CLI!

If the output is messy, even the best logic is useless. This Display is like formal clothes for a program, without it it works, but with clothes, it looks professional.

Ideas for the Future

  • [ ] Add color with colorama or rich
  • [ ] Different themes (minimal, detailed)
  • [ ] Table with tabulate
  • [ ] More icons (book, author, calendar)

In summary:

Display is not just "printing", it's an art.

This small module turned the program from a simple script into a pleasant and professional tool.


5. Main Menu: Where the User Talks to the Program

So far we had everything: storage, logic, search, nice display. But how does the user interact with the program? Here we can't just use raw print and input. We need a command-line user interface (CLI) that:

  • Is simple
  • Doesn't confuse the user
  • Handles mistakes gracefully
  • Has option to cancel operations

All this is gathered in cli.py. I created a class LibraryApp that is the interactive brain of the program. Only here the user enters, selects, gives info, and gets results.

Let's see in detail how it works.

Main Code for LibraryApp

# cli.py
from ui.cli_display import Display
from src.library_core import Library

class LibraryApp:
    def __init__(self):
        self.library = Library()  # Program core

    def main_menu(self):
        while True:
            Display.menu()
            choice = input("\nYour choice: ").strip()

            if choice == '1':
                self.add_book_flow()
            elif choice == '2':
                self.list_books_flow()
            # ... other options
            elif choice == '7':
                Display.info("Goodbye! See you next time!")
                break
            else:
                Display.warning("Invalid choice! Try again.")
Enter fullscreen mode Exit fullscreen mode
  • self.library = Library() → sets up the program core
  • main_menu() → has an infinite loop until user selects 7 (exit)
  • Display.menu() → shows the nice menu (which I'll say what it is later)
  • input() → gets user choice
  • Each option has a separate method (like add_book_flow) that does its job

Method get_valid_year(), Year Validation with Cancel Option

def get_valid_year(self):
    while True:
        year = input("Publication year (or 'cancel'): ").strip()
        if year.lower() == 'cancel':
            return None
        if year.isdigit() and 1000 <= int(year) <= 2025:
            return int(year)
        Display.warning("Year must be between 1000 and 2025!")
Enter fullscreen mode Exit fullscreen mode

This method is the heart of input validation for the book publication year. Why so important? Because:

  • User might type abc, 1999 (Persian), -500, 2030
  • I don't want the program to crash
  • I don't want the user to get stuck
Line by Line:
  1. while True: → infinite loop, continues until correct input
  2. input("... (or 'cancel')") → user knows can cancel
  3. .strip() → removes extra spaces
  4. if year.lower() == 'cancel': return None → user can go back anytime
  5. year.isdigit() → checks if only numbers (no letters, no signs)
  6. 1000 <= int(year) <= 2025 → logical range (from 1000 to 2025)
  7. return int(year) → converts number to int
  8. Display.warning(...) → if wrong, warns in a friendly tone

Note: Why 2025? Because it's now 2025 (per system date), and I don't want user to enter future year!

Add Book Flow (add_book_flow)

def add_book_flow(self):
    title = input("Book title (or 'cancel'): ")
    if title.lower() == 'cancel':
        Display.info("Returning to main menu.")
        return

    author = input("Author name (or 'cancel'): ")
    if author.lower() == 'cancel':
        Display.info("Returning to main menu.")
        return

    year = self.get_valid_year()
    if year is None:
        Display.info("Returning to main menu.")
        return

    result = self.library.add_book(title, author, year)
    if result["success"]:
        Display.success(result["message"])
    else:
        Display.warning(result["message"])
Enter fullscreen mode Exit fullscreen mode

This method manages the full flow of adding a book:

  1. Gets title → if cancel, returns
  2. Gets author → same
  3. Gets year with get_valid_year() → if None, cancel
  4. self.library.add_book(...) → does the main work
  5. Shows result with Display.success or warning

Everywhere there's cancel option. User never gets stuck.

Main Menu, with Display.menu()

In cli_display.py we have this method:

@staticmethod
def menu():
    Display.header("Personal Library Manager")
    print("1️⃣  Add book")
    print("2️⃣  Display all books")
    print("3️⃣  Search book")
    print("4️⃣  Delete book")
    print("5️⃣  Edit book")
    print("6️⃣  Library summary")
    print("7️⃣  Exit")
    print("" * 50)
Enter fullscreen mode Exit fullscreen mode

Output in terminal:

 ─────────────────────────────
    Personal Library Manager
 ─────────────────────────────
1️⃣  Add book
2️⃣  Display all books
3️⃣  Search book
4️⃣  Delete book
5️⃣  Edit book
6️⃣  Library summary
7️⃣  Exit
──────────────────────────────────────────────────
Enter fullscreen mode Exit fullscreen mode

Nice, clear, and professional!

My Real Experience

The first time I ran the menu, it was like this:

Your choice: 1
Book title (or 'cancel'): 1984
Author name (or 'cancel'): George Orwell
Publication year (or 'cancel'): 1949
Book '1984' added.
Enter fullscreen mode Exit fullscreen mode

Then I hit 2 → list came → I got excited!

Then once I mistakenly typed abc for year:

Publication year (or 'cancel'): abc
Year must be between 1000 and 2025!
Publication year (or 'cancel'): cancel
Returning to main menu.
Enter fullscreen mode Exit fullscreen mode

No crash, no stuck. Everything smooth and human.

Why Is This Structure Good?

Advantage Explanation
Cancel Option User can type cancel at any moment
Smart Validation Only accepts correct input
Clear Messages User knows exactly what happened
Separate Flows Each operation has a separate method
Extensible Adding new option just needs an elif

Ideas for the Future

  • [ ] Multi-page menu if books increase
  • [ ] Command history (like undo)
  • [ ] Confirmation before delete (Are you sure?)
  • [ ] Auto-save user settings

In summary:

Main menu is like the library's entrance door.

If confusing, user leaves. But if simple, safe, and friendly, they come back every day.

This cli.py is exactly that: friendly, smart, and reliable.


6. Execution

# main.py
if __name__ == "__main__":
    app = LibraryApp()
    app.main_menu()
Enter fullscreen mode Exit fullscreen mode

Runs with python main.py.


📝 My Real Experience

The first time I ran it, I added two books:

#1  1984
     Author: George Orwell
     Year: 1949

#2  The Little Prince
     Author: Antoine de Saint-Exupéry
     Year: 1943
Enter fullscreen mode Exit fullscreen mode

Then closed the program, reopened, they were still there! It felt really good.

Then went to delete. I had added a wrong book, deleted it, JSON file updated too. Everything automatic.


⚠️ Small Problems I Had

  • At first, didn't check year, someone entered 3000! Later added validation.
  • Persian names saved corrupted, fixed with ensure_ascii=False.
  • If two books have same title, deleting was problematic. For now, only allow deleting the first and warn.

📖 What Did I Learn?

  • How to do proper modularization
  • How easy persistent storage with JSON is
  • How important input validation is
  • Even a small program can feel good when it works

🔮 If I Want to Improve It?

  • [ ] Add genre or reading status
  • [ ] Search by author
  • [ ] Output as table with tabulate
  • [ ] Automatic backup of JSON file
  • [ ] Web version with Flask (maybe later!)

📺 Watch Personal Library Manager in Action!

To see the clean CLI menus, smooth book adding, search, delete, edit, and instant JSON persistence, all in pure Python, check out the demo video on YouTube. It’s a quick 2-minute tour to feel the vibe of a real, usable personal tool you can run every day


📦 Source Code on GitHub!

Want to see it in action? The full source code will be available soon in the GitHub repository. I'll update the link by the end of the week, promise!

To get a feel for the simple CLI menus, book adding, listing, and search features, check back for the repo. It's a quick way to see what you can build with pure Python for everyday use!


🏁 Final Words

This project is very simple, but it's really been useful to me. I add a book to it every day. If you read a lot too, I suggest making a copy and modifying it.

What tools do you use to manage your books? Goodreads? Notes? Or like me, a handmade program? Say in the comments!

Top comments (0)