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…
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.jsonfile, close, reopen, everything’s there (UTF-8 for Persian/any language). - Polished Terminal UI: Custom headers, indented displays, success/warning messages, and
cancelanywhere 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
⚙️ 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
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}")
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 []
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
}
]
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
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()
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."}
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)
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}
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."}
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."}
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."}
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}
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
Librarycode 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()]
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")
Without writing engine = SearchEngine().
How Does find_by_title(books, title) Work?
This method takes two inputs:
books: list of books (same asself.booksfromLibraryclass)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()]
Let’s break it line by line:
book for book in books→ examines all books one by oneif title.lower() in book['title'].lower()→ main condition:
title.lower()→ converts what the user entered to lowercasebook['title'].lower()→ converts the book title to lowercase tooin→ 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."}
Meaning:
- Delegates the search to
SearchEngine - Gets the result (list of books)
- If empty → error message
- 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}
]
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:
Separation of Concerns
Library just manages, SearchEngine just searches.
1.
Testable Separately
I can write a test that only tries SearchEngine, without needing Library.
1.
Extensible
Later I can add these:
@staticmethod
def find_by_author(books, author):
...
@staticmethod
def advanced_search(books, query):
# Combined search: title + author + year
- Code Readability When you write
SearchEngine.find_by_title(...)inLibrary, 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")
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}")
This method creates a text box. For example, if the title is "My Books":
────────────────────────
My Books
────────────────────────
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")
This method takes the book list and displays it nicely and orderly.
Line by Line:
if not books:→ if list empty, says a simple message and doneDisplay.header("My Books")→ first puts a nice titleenumerate(books, 1)→ numbering starts from 1 (not 0)- Each book has three lines:
#{i} {title}→ number + titleAuthor: {author}→ with 5 spaces indentationYear: {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
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}")
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"])
Meaning:
Libraryjust returns data and statusDisplayjust 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
coloramaorrich - [ ] 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.")
self.library = Library()→ sets up the program coremain_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!")
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:
while True:→ infinite loop, continues until correct inputinput("... (or 'cancel')")→ user knows can cancel.strip()→ removes extra spacesif year.lower() == 'cancel': return None→ user can go back anytimeyear.isdigit()→ checks if only numbers (no letters, no signs)1000 <= int(year) <= 2025→ logical range (from 1000 to 2025)return int(year)→ converts number tointDisplay.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"])
This method manages the full flow of adding a book:
- Gets title → if
cancel, returns - Gets author → same
- Gets year with
get_valid_year()→ ifNone, cancel self.library.add_book(...)→ does the main work- Shows result with
Display.successorwarning
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)
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
──────────────────────────────────────────────────
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.
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.
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()
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
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!