Build a Random Book Picker in Python
Build a dark-mode desktop app that picks random books from your library.
Introduction
30 Second Summary
You know that feeling when you have a shelf full of books and can't decide what to read next? Sometimes you just need something to make the choice for you.
In this project, you will build a sleek desktop application with Python that manages your personal reading list and picks a random book for you at the click of a button.
What You'll Build
A modern dark-mode desktop application built with CustomTkinter that stores your book collection in a JSON file and randomly selects your next read.
By the end of this project, you'll have:
- A modern dark-themed GUI application with input fields, buttons, and a scrollable book list.
- A JSON-based data layer that persists your book collection between sessions and can be managed entirely through the interface.
- An event-driven application with class-based structure that responds to button clicks and user input.
- Secret Mission: Add a genre filter dropdown so you can get a random pick from a specific genre.
Are there any prerequisites?
None. This guide includes full setup instructions for Python, CustomTkinter, and your code editor. You do not need any prior programming experience to follow along.
Before We Start
You're about to build a desktop application that picks random books from your personal library. Before you write any code, you need a few tools in place.
In this step, you'll install Python, set up your code editor, and add the library that gives your app its modern dark-mode look.
In this step, get ready to:
- Install Python 3.13 on your system.
- Create a project folder and open it in your editor.
- Install the CustomTkinter package.
Install Python on your system
Python is the programming language you'll use to build your book picker app. This project requires Python 3.13 or higher.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or PowerShell (Windows), and pressing Enter.
- Check your Python version by running this command:
python3 --version
You should see output like Python 3.13.x where x is any number.
✔️ I see version 3.13 or higher
Python is ready to go. Skip ahead to the next substep.
ⓧ I see an older version
Your system has an older Python installed. You need to install Python 3.13 alongside it.
- Visit python.org/downloads in your browser.
- Download the Python 3.13 installer for your operating system.
- Run the installer and follow the prompts.
- On Windows, make sure to check Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version again to confirm it shows 3.13.
Why does macOS still show an older version?
macOS often ships with an older Python version by default. The python.org installer adds the new version to your PATH, so python3 --version should resolve to 3.13 after a terminal restart.
If it still shows the old version, try running python3.13 --version directly. If that works, use python3.13 and pip3.13 for all commands in this project.
ⓧ Command not found
Python is not installed on your system yet. Follow these steps to install it.
- Visit python.org/downloads in your browser.
- Download the Python 3.13 installer for your operating system.
- Run the downloaded installer.
- On macOS, double-click the .pkg file and follow the prompts.
- On Windows, check Add Python to PATH and click Install Now.
- Close and reopen your terminal after installation.
- Run python3 --version again. You should see Python 3.13.x in the output.
Still seeing command not found?
On Windows, try typing python --version (without the 3). The Windows installer sometimes creates the command as just python.
If nothing works, make sure you closed and reopened your terminal after installing. The PATH changes only take effect in new terminal sessions.
help me install Python 3.13 on my computer
Create your project folder and open it in your editor
You need a dedicated folder to hold your project files. You'll also need a code editor to write your Python code.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir book-picker
cd book-picker
- Confirm the folder was created by running:
pwd
You should see a path ending in book-picker.
Now open your code editor. This project recommends Cursor, but VS Code works too.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor (or VS Code) and press Enter to open it.
Don't have Cursor or VS Code installed?
Download Cursor from cursor.com or VS Code from code.visualstudio.com. Both are free. Install whichever you prefer, then come back here.
- In your editor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the book-picker folder, then click Open.
You should see the book-picker folder name in your editor's sidebar with no files inside yet.
Don't see the folder in your sidebar?
Make sure you opened the book-picker folder itself, not the Desktop. The folder name should appear at the top of the sidebar file tree.
help me open my project folder in my code editor
Install CustomTkinter
CustomTkinter is a Python library that provides modern-looking GUI widgets with dark mode support. It builds on top of tkinter (which ships with Python) and adds rounded buttons, color themes, and a polished appearance.
- Open your editor's built-in terminal by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
- Install CustomTkinter by running this command:
pip3 install customtkinter==5.2.2
What does this command do?
pip3 is Python's package manager. It downloads and installs libraries from the Python Package Index (PyPI).
The ==5.2.2 part pins the exact version so your app behaves the same way as this guide.
You should see output ending with a line that says the package was successfully installed.
Seeing a permission error or pip not found?
If you see a permission error, try adding --user to the end of the command. If pip3 is not found, try pip instead (without the 3).
If you used python3.13 earlier because of version issues, use pip3.13 here too.
help me install customtkinter with pip
Your environment is ready. Next up, you'll create the data layer that stores and loads your book library from a JSON file.
Set Up Python and Install CustomTkinter
Before you can build a GUI app, you need the right tools in place. This project requires Python 3.13 and a library called CustomTkinter that gives you modern-looking widgets out of the box.
In this step, you'll confirm Python is ready, create your project folder, and install the one external package the app depends on.
In this step, get ready to:
- Verify Python 3.13 is installed on your system.
- Create a project folder and open it in your code editor.
- Install CustomTkinter for building modern GUI interfaces.
Verify Python 3.13 is installed
Python is the programming language you'll use to build the entire book picker app. You need version 3.13 or higher for this project.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or Command Prompt (Windows), and pressing Enter.
- Check your Python version by running this command:
python3 --version
✔️ I see version 3.13 or higher
You should see something like Python 3.13.5 (or any 3.13.x version). You're all set to continue.
ⓧ I see an older version
If you see a version lower than 3.13 (like 3.9 or 3.11), you need to install Python 3.13 separately. macOS often ships with an older version by default.
- Visit python.org/downloads and download the Python 3.13 installer for your operating system.
- Run the installer and follow the prompts.
- On Windows, make sure to check Add Python to PATH during installation.
- After installation, close and reopen your terminal, then run python3 --version again to confirm it now shows 3.13.x.
macOS users with an older default Python
Even after installing Python 3.13, your system might still point python3 to the older version. Try running python3.13 --version instead. If that works, use python3.13 and pip3.13 throughout the project wherever you see python3 or pip3.
ⓧ Command not found
Python is not installed on your system yet. Follow these steps to install it:
- Visit python.org/downloads and download the Python 3.13 installer for your operating system.
- On macOS, download the macOS 64-bit universal2 installer and run it.
- On Windows, download the Windows installer (64-bit) and run it. Make sure to check Add Python to PATH on the first screen.
- After installation, close and reopen your terminal, then run python3 --version to confirm it shows 3.13.x.
Create your project folder and open it in your editor
You need a dedicated folder to keep your project files organized. This is where your Python script and book data will live.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir book-picker
cd book-picker
- Confirm the folder was created by running:
pwd
You should see a path ending in book-picker (something like /Users/yourname/Desktop/book-picker).
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor (or VS Code if you prefer) and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the book-picker folder, and click Open.
Why Cursor or VS Code?
Both Cursor and VS Code are code editors that give you a file explorer, syntax highlighting, and a built-in terminal. Either works perfectly for this project.
- Open the built-in terminal in your editor by pressing Ctrl+` (backtick key, located below Escape on most keyboards).
Your terminal should open directly inside the book-picker folder. You'll use this terminal for the rest of the project.
Terminal opened in the wrong folder?
If the terminal prompt doesn't show book-picker, run cd ~/Desktop/book-picker to navigate there manually.
Help me get my terminal to the right folder.
Install CustomTkinter
CustomTkinter is a Python library that provides modern, dark-mode-ready widgets for building desktop apps. It wraps the built-in tkinter library with a polished look and feel.
- Install CustomTkinter by running this command in your terminal:
pip3 install customtkinter==5.2.2
What does this command do?
pip3 is Python's package manager. It downloads and installs libraries from the Python Package Index (PyPI).
The ==5.2.2 part pins the exact version so everyone following this project gets the same behavior.
- Verify the installation worked by running:
python3 -c "import customtkinter; print(customtkinter.__version__)"
You should see 5.2.2 printed in your terminal. This confirms CustomTkinter is installed and importable.
Seeing a ModuleNotFoundError?
Make sure you ran the pip3 install command in the same terminal where you're testing the import. If you installed Python 3.13 alongside an older version, try pip3.13 install customtkinter==5.2.2 instead.
Help me fix my CustomTkinter installation.
Your environment is locked and loaded. Next up, you'll create the data layer that stores your book collection.
Create the Data Layer
Your development environment is ready with Python and CustomTkinter installed. Now it's time to build the data foundation your app will run on.
Every app needs a way to store and retrieve data. You'll create a JSON file that holds your book collection and write two Python functions to load and save it.
In this step, get ready to:
- Create a starter book library stored in JSON.
- Write a function to load books from the file.
- Write a function to save changes back to the file.
Create the starter book library
Your book collection lives in a JSON file so it persists between sessions. JSON is a text-based format that stores structured data as key-value pairs.
- Create a new file called library.json in your project folder.
- Paste the following starter data into library.json:
[
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Sci-Fi"
},
{
"title": "The Hobbit",
"author": "J.R.R. Tolkien",
"genre": "Fantasy"
},
{
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian"
}
]
What does this file do?
This JSON file is your app's database. It stores an array of book objects, each with three fields:
- title is the book's name.
- author is who wrote it.
- genre is the category for filtering later.
- Save library.json.
- Confirm the file exists by running:
ls
- You should see library.json in the output.
Don't see library.json?
- Make sure your terminal is in the project folder. Run pwd to check your current directory.
- Confirm you saved the file inside the project folder, not in a parent or child directory.
Why isn't my library.json showing up in my project folder?
✔️ Awesome, I've got everything!
Great. Make sure you've saved library.json and let's move on.
ⓧ I'd like to double check the full code
[
{
"title": "Dune",
"author": "Frank Herbert",
"genre": "Sci-Fi"
},
{
"title": "The Hobbit",
"author": "J.R.R. Tolkien",
"genre": "Fantasy"
},
{
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian"
}
]
Write the load_library function
Your app needs a way to read the JSON file and turn it into a Python list. The load_library() function handles this. It also gracefully handles the case where the file doesn't exist yet.
- Create a new file called book_picker.py in your project folder.
- Add the following imports and function to book_picker.py:
import json
import random
import customtkinter
def load_library(filepath="library.json"):
"""Load the book library from a JSON file."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
What does this code do?
- import json and import random bring in Python's built-in modules for reading JSON files and making random selections.
- import customtkinter brings in the GUI library you installed in Step 1.
- load_library() opens the JSON file, reads its contents, and returns them as a Python list of dictionaries.
- The try/except block returns an empty list if the file doesn't exist. This prevents your app from crashing on first launch.
- encoding="utf-8" ensures the file handles special characters in book titles or author names correctly.
- Save book_picker.py.
You won't see output yet. The next function completes the data layer, and then you'll test them both together.
Write the save_library function
When the learner adds or removes a book, the app needs to write the updated list back to the JSON file. The save_library() function handles this persistence.
- In book_picker.py, add the following function below load_library():
def save_library(library, filepath="library.json"):
"""Save the book library to a JSON file."""
with open(filepath, "w", encoding="utf-8") as f:
json.dump(library, f, indent=4)
What does this code do?
- save_library() takes the current book list and writes it to the JSON file.
- json.dump() converts the Python list back into JSON text.
- indent=4 formats the output with nice spacing so the file stays human-readable.
- The "w" mode overwrites the file each time, replacing old data with the current state.
- Save book_picker.py.
- Test that everything works by running:
python3 book_picker.py
- You should return to your terminal prompt with no errors. This confirms your imports are working and your functions have no syntax errors.
Seeing a ModuleNotFoundError?
- If you see ModuleNotFoundError: No module named 'customtkinter', your terminal is using a different Python than the one where you installed the package. Run pip3 install customtkinter==5.2.2 again in the same terminal.
- On Linux, if you see an error about tkinter not being found, install it with your system package manager (e.g. sudo apt-get install python3-tk on Ubuntu/Debian).
- Check for typos in your import statements. The module name is customtkinter (all lowercase, no spaces).
Why am I getting a ModuleNotFoundError when running my book_picker.py?
✔️ Awesome, I've got everything!
Great. Double check you've saved book_picker.py and let's keep going.
ⓧ I'd like to double check the full code
import json
import random
import customtkinter
def load_library(filepath="library.json"):
"""Load the book library from a JSON file."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
def save_library(library, filepath="library.json"):
"""Save the book library to a JSON file."""
with open(filepath, "w", encoding="utf-8") as f:
json.dump(library, f, indent=4)
Your data layer is complete. Next up, you'll build the application window and start seeing your book picker come to life on screen.
Build the Application Window
Your data layer can already read and write books to library.json. Now it's time to build the window your users will actually see and interact with.
CustomTkinter uses a class-based pattern where your entire app lives inside a class that inherits from the main window object. In this step, you'll create that class, configure the dark-mode layout, and add input fields for entering new books.
In this step, get ready to:
- Create the BookPickerApp class with window configuration.
- Set up dark mode appearance and run the app for the first time.
- Add the title label and book input fields with an Add button.
Create the main BookPickerApp class
Every CustomTkinter app starts with a class that inherits from customtkinter.CTk. This class IS your main window. Everything you add inside it becomes part of the GUI.
- In book_picker.py, find the save_library() function. Add the following class below it:
class BookPickerApp(customtkinter.CTk):
def __init__(self):
super().__init__()
self.title("Book Picker")
self.geometry("600x700")
self.library = load_library()
# Configure grid layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(3, weight=1)
What does this code do?
This creates the skeleton of your app window.
- class BookPickerApp(customtkinter.CTk) makes your class inherit from the main CustomTkinter window.
- super().__init__() initializes the parent window so it actually works.
- self.title() and self.geometry() set the window name and size (600 pixels wide, 700 tall).
- self.library = load_library() loads your books from library.json into memory when the app starts.
- The two grid_columnconfigure and grid_rowconfigure lines set up a grid layout system so widgets stretch to fill available space.
You won't see a change yet because the app doesn't have a way to launch. The next chunk adds that.
Configure dark mode appearance and launch the app
CustomTkinter's dark mode is set globally before the window is created. The standard Python pattern if __name__ == "__main__" is where you configure appearance and start the app's event loop.
- At the very bottom of book_picker.py (after the class definition), add this launch block:
if __name__ == "__main__":
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("blue")
app = BookPickerApp()
app.mainloop()
What does this code do?
This block only runs when you execute the script directly (not when importing it as a module).
- set_appearance_mode("dark") activates the dark theme for all widgets.
- set_default_color_theme("blue") sets accent colors like button backgrounds to blue.
- app.mainloop() starts the event loop that keeps the window open and listening for clicks.
- Save book_picker.py.
- Run your app by entering this command in your terminal:
python3 book_picker.py
You should see a dark-themed empty window titled "Book Picker" appear on your screen.
Don't see the window?
- Make sure library.json is in the same folder as book_picker.py. The load_library() function looks for it in the current directory.
- Check that you're running the command from inside your project folder (book-picker).
- If you see ModuleNotFoundError: No module named 'customtkinter', run pip3 install customtkinter==5.2.2 again.
Help me debug why my CustomTkinter window isn't appearing.
- Close the window before continuing (click the X button or press Cmd+Q on macOS / Alt+F4 on Windows).
Add the title label and the Add Book input section
Now you'll add the visible widgets. CustomTkinter provides modern-looking labels, entry fields, and buttons that slot into your grid layout.
- Inside the __init__ method, find the line self.grid_rowconfigure(3, weight=1). Add the title label below it:
# Title
self.title_label = customtkinter.CTkLabel(
self, text="Book Picker", font=("", 24, "bold")
)
self.title_label.grid(row=0, column=0, padx=20, pady=(20, 10))
What does this code do?
This creates a large bold heading at the top of the window. The font=("", 24, "bold") tuple uses the system default font at 24px bold. The .grid() call places it in row 0 with padding around the edges.
You won't see this change until you run the app again. First, add the input section below.
- Directly below the title label code, add the Add Book section with three entry fields and a button:
# Add Book Section
self.add_frame = customtkinter.CTkFrame(self)
self.add_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
self.add_frame.grid_columnconfigure((0, 1, 2), weight=1)
self.title_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Title"
)
self.title_entry.grid(row=0, column=0, padx=5, pady=10, sticky="ew")
self.author_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Author"
)
self.author_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
self.genre_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Genre"
)
self.genre_entry.grid(row=0, column=2, padx=5, pady=10, sticky="ew")
self.add_button = customtkinter.CTkButton(
self.add_frame, text="Add Book", command=self.add_book
)
self.add_button.grid(row=1, column=0, columnspan=3, padx=5, pady=(0, 10))
What does this code do?
This creates a framed input section with three fields side by side.
- CTkFrame groups the input fields into a visual container with sticky="ew" so it stretches horizontally.
- grid_columnconfigure((0, 1, 2), weight=1) makes all three columns share space equally.
- Each CTkEntry shows placeholder text ("Title", "Author", "Genre") that disappears when you type.
- The CTkButton spans all three columns with columnspan=3 and references self.add_book which you'll define in a later step.
- Save book_picker.py.
Why you can't run the app yet
The Add button references self.add_book, a method that doesn't exist yet. Python evaluates this reference immediately when creating the button, so running the script now raises an AttributeError. You'll define add_book in Step 5. For now, your code is correct and ready for the next additions.
Seeing a different error?
- Check that your indentation is consistent. Every line inside __init__ must be indented with 8 spaces (two levels in from the class).
- Make sure the Add Book section code is inside the __init__ method, not outside the class.
Help me fix an error in my BookPickerApp class.
✔️ Awesome, I've got everything!
Great. Double check you've saved book_picker.py and move on to the next step.
ⓧ I'd like to double check the full code
import json
import random
import customtkinter
def load_library(filepath="library.json"):
"""Load the book library from a JSON file."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
def save_library(library, filepath="library.json"):
"""Save the book library to a JSON file."""
with open(filepath, "w", encoding="utf-8") as f:
json.dump(library, f, indent=4)
class BookPickerApp(customtkinter.CTk):
def __init__(self):
super().__init__()
self.title("Book Picker")
self.geometry("600x700")
self.library = load_library()
# Configure grid layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(3, weight=1)
# Title
self.title_label = customtkinter.CTkLabel(
self, text="Book Picker", font=("", 24, "bold")
)
self.title_label.grid(row=0, column=0, padx=20, pady=(20, 10))
# Add Book Section
self.add_frame = customtkinter.CTkFrame(self)
self.add_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
self.add_frame.grid_columnconfigure((0, 1, 2), weight=1)
self.title_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Title"
)
self.title_entry.grid(row=0, column=0, padx=5, pady=10, sticky="ew")
self.author_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Author"
)
self.author_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
self.genre_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Genre"
)
self.genre_entry.grid(row=0, column=2, padx=5, pady=10, sticky="ew")
self.add_button = customtkinter.CTkButton(
self.add_frame, text="Add Book", command=self.add_book
)
self.add_button.grid(row=1, column=0, columnspan=3, padx=5, pady=(0, 10))
if __name__ == "__main__":
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("blue")
app = BookPickerApp()
app.mainloop()
Your application window is structured and ready for content. Next up, you'll fill that empty space below the inputs with a scrollable book library.
Display the Book Library
Your app has a window with input fields, but there's no way to actually see the books in your library. Right now the three starter books sit silently in library.json with no visual representation.
In this step, you'll create a scrollable list that displays every book in your collection. Each book gets its own row with a Remove button so you can manage your library directly from the GUI.
In this step, get ready to:
- Create a scrollable frame to hold your book list.
- Write a method that dynamically renders books from your library.
- Add a Remove button next to each book entry.
Add a scrollable frame to the window
A regular frame would clip your book list if it grows too long. CustomTkinter provides a CTkScrollableFrame widget that automatically adds a scrollbar when its content overflows.
- In book_picker.py, find the self.add_button.grid(...) line inside __init__.
- Add the following code below it (after the Add Book Section):
# Book Library (Scrollable)
self.library_frame = customtkinter.CTkScrollableFrame(
self, label_text="Your Library"
)
self.library_frame.grid(row=3, column=0, padx=20, pady=(10, 20), sticky="nsew")
self.library_frame.grid_columnconfigure(0, weight=1)
self.refresh_library()
What does this code do?
- CTkScrollableFrame creates a container with a built-in scrollbar. The label_text="Your Library" adds a heading at the top of the frame.
- sticky="nsew" stretches the frame in all directions to fill the available space.
- self.refresh_library() calls a method you'll write next that fills this frame with books.
You won't see a change yet because refresh_library() doesn't exist. The next chunk creates it.
Write the refresh_library() method
Every time the book list changes (a book is added or removed), the app needs to redraw the entire list. This method clears the scrollable frame and rebuilds it from the current state of self.library.
- Below the __init__ method (still inside the BookPickerApp class), add this new method:
def refresh_library(self):
"""Clear and re-render the book list."""
for widget in self.library_frame.winfo_children():
widget.destroy()
if not self.library:
empty_label = customtkinter.CTkLabel(
self.library_frame, text="No books yet. Add some above!"
)
empty_label.grid(row=0, column=0, padx=10, pady=10)
return
for i, book in enumerate(self.library):
book_text = f"\"{ book['title'] }\" by {book['author']} [{book['genre']}]"
book_label = customtkinter.CTkLabel(
self.library_frame, text=book_text, anchor="w"
)
book_label.grid(row=i, column=0, padx=10, pady=2, sticky="w")
remove_button = customtkinter.CTkButton(
self.library_frame, text="Remove", width=70,
fg_color="transparent", border_width=1,
command=lambda idx=i: self.remove_book(idx)
)
remove_button.grid(row=i, column=1, padx=5, pady=2)
What does this method do?
- winfo_children() gets every widget currently inside the frame. Calling .destroy() on each one wipes the slate clean before re-rendering.
- The if not self.library check handles the empty state by showing a friendly message instead of a blank frame.
- The for loop creates a label and a Remove button for each book. Each book gets its own row in the grid.
- lambda idx=i captures the current loop index. Without this, every button would reference the last value of i and remove the wrong book.
Why a transparent Remove button?
Setting fg_color="transparent" with border_width=1 creates a subtle outlined button. This keeps the Remove action available without visually competing with the book titles.
- Save book_picker.py.
You won't be able to run the app yet because the add_book and remove_book methods haven't been written. The next step wires everything together and brings the app to life.
Seeing a syntax error when you save?
- Make sure refresh_library is indented one level inside the BookPickerApp class (same level as def __init__).
- Check that the f-string uses matching quotes. The escaped quotes \" inside the f-string wrap the book title.
- Confirm the self.refresh_library() call at the end of __init__ comes AFTER the self.library_frame creation.
Need more help?
✔️ Awesome, I've got everything!
Great. Double check you've saved book_picker.py before moving on.
ⓧ I'd like to double check the full code
Here's what your complete book_picker.py should look like at this point:
import json
import random
import customtkinter
def load_library(filepath="library.json"):
"""Load the book library from a JSON file."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
def save_library(library, filepath="library.json"):
"""Save the book library to a JSON file."""
with open(filepath, "w", encoding="utf-8") as f:
json.dump(library, f, indent=4)
class BookPickerApp(customtkinter.CTk):
def __init__(self):
super().__init__()
self.title("Book Picker")
self.geometry("600x700")
self.library = load_library()
# Configure grid layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(3, weight=1)
# Title
self.title_label = customtkinter.CTkLabel(
self, text="Book Picker", font=("", 24, "bold")
)
self.title_label.grid(row=0, column=0, padx=20, pady=(20, 10))
# Add Book Section
self.add_frame = customtkinter.CTkFrame(self)
self.add_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
self.add_frame.grid_columnconfigure((0, 1, 2), weight=1)
self.title_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Title"
)
self.title_entry.grid(row=0, column=0, padx=5, pady=10, sticky="ew")
self.author_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Author"
)
self.author_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
self.genre_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Genre"
)
self.genre_entry.grid(row=0, column=2, padx=5, pady=10, sticky="ew")
self.add_button = customtkinter.CTkButton(
self.add_frame, text="Add Book", command=self.add_book
)
self.add_button.grid(row=1, column=0, columnspan=3, padx=5, pady=(0, 10))
# Book Library (Scrollable)
self.library_frame = customtkinter.CTkScrollableFrame(
self, label_text="Your Library"
)
self.library_frame.grid(row=3, column=0, padx=20, pady=(10, 20), sticky="nsew")
self.library_frame.grid_columnconfigure(0, weight=1)
self.refresh_library()
def refresh_library(self):
"""Clear and re-render the book list."""
for widget in self.library_frame.winfo_children():
widget.destroy()
if not self.library:
empty_label = customtkinter.CTkLabel(
self.library_frame, text="No books yet. Add some above!"
)
empty_label.grid(row=0, column=0, padx=10, pady=10)
return
for i, book in enumerate(self.library):
book_text = f"\"{ book['title'] }\" by {book['author']} [{book['genre']}]"
book_label = customtkinter.CTkLabel(
self.library_frame, text=book_text, anchor="w"
)
book_label.grid(row=i, column=0, padx=10, pady=2, sticky="w")
remove_button = customtkinter.CTkButton(
self.library_frame, text="Remove", width=70,
fg_color="transparent", border_width=1,
command=lambda idx=i: self.remove_book(idx)
)
remove_button.grid(row=i, column=1, padx=5, pady=2)
if __name__ == "__main__":
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("blue")
app = BookPickerApp()
app.mainloop()
Your library display is built. Next up, you'll add the random pick feature and connect the Add button so the app is fully interactive.
Add the Random Pick Feature
Your app has a window, input fields, and a scrollable book list. But it can't actually do anything yet. The buttons reference methods that don't exist.
In this step, you'll bring the app to life by adding the random pick feature and wiring up all the button actions. By the end, you'll have a fully working book picker application.
In this step, get ready to:
- Add a "Pick a Random Book" button and a result display label.
- Write the method that picks a random book using random.choice().
- Wire up the Add and Remove buttons to manage your library.
Add the pick button and result label
The random pick feature needs two UI elements: a large button the user clicks, and a label that displays the result.
- In book_picker.py, find the # Book Library (Scrollable) comment inside the __init__ method.
- Add the following code directly above that comment:
# Pick Random Button
self.pick_button = customtkinter.CTkButton(
self, text="Pick a Random Book", command=self.pick_random_book,
font=("", 16, "bold"), height=40
)
self.pick_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
# Result Label
self.result_label = customtkinter.CTkLabel(
self, text="", font=("", 16), text_color=("#1f6aa5", "#5dade2")
)
self.result_label.grid(row=2, column=0, padx=20, pady=(60, 0))
What does this code do?
self.pick_button creates a bold, tall button that will call self.pick_random_book when clicked. The height=40 makes it larger than a standard button so it stands out as the main action.
self.result_label is an empty label that will display the randomly picked book. The text_color tuple provides two colors: the first for light mode, the second for dark mode. This gives the result a highlighted blue appearance.
You won't see a change yet. The next chunk defines the method the button calls.
Write the random book picker method
Python's random module has a function called random.choice() that returns a random element from any non-empty list. This is the core of the pick feature.
- Scroll down to the bottom of the BookPickerApp class (after the refresh_library method).
- Add the pick_random_book method by copying this code:
def pick_random_book(self):
"""Pick a random book from the library."""
if not self.library:
self.result_label.configure(text="Library is empty! Add some books first.")
return
book = random.choice(self.library)
self.result_label.configure(
text=f"Read this: \"{book['title']}\" by {book['author']}"
)
What does this code do?
The method first checks whether the library is empty. If it is, it shows a friendly message and stops early with return.
If there are books, random.choice(self.library) picks one at random. The result label then updates to show the title and author of the chosen book.
- Save book_picker.py.
- Run the app by entering this command in your terminal:
python3 book_picker.py
- Click the Pick a Random Book button. You should see a blue text recommendation appear (e.g. "Read this: "Dune" by Frank Herbert").
- Click the button a few more times. The recommendation changes each time.
- Close the app window when you're done testing.
App crashes when you click the button?
If you see an error mentioning pick_random_book, make sure the method is indented at the same level as refresh_library. It must be inside the BookPickerApp class (indented with 4 spaces).
Also check that you have import random at the top of your file. Without it, Python doesn't know what random.choice means.
Help me debug my pick_random_book method
Wire up the add and remove methods
The Add Book button and Remove buttons already exist in your code, but they reference methods that haven't been written yet. Time to define them.
- In book_picker.py, add the add_book method after refresh_library (and before pick_random_book):
def add_book(self):
"""Add a new book from the entry fields."""
title = self.title_entry.get().strip()
author = self.author_entry.get().strip()
genre = self.genre_entry.get().strip()
if not title or not author or not genre:
self.result_label.configure(text="Please fill in all fields.")
return
book = {"title": title, "author": author, "genre": genre}
self.library.append(book)
save_library(self.library)
# Clear entries
self.title_entry.delete(0, "end")
self.author_entry.delete(0, "end")
self.genre_entry.delete(0, "end")
self.result_label.configure(text=f"Added \"{title}\"!")
self.refresh_library()
What does this code do?
The method reads what the user typed into all three entry fields using .get().strip(). The .strip() removes any accidental whitespace from the start and end.
If any field is empty, it shows a warning message and stops. Otherwise, it creates a new book dictionary, adds it to the library list, and saves to library.json. Then it clears the input fields and refreshes the book list display.
- Now add the remove_book method directly below add_book:
def remove_book(self, index):
"""Remove a book by its index."""
removed = self.library.pop(index)
save_library(self.library)
self.result_label.configure(text=f"Removed \"{removed['title']}\"")
self.refresh_library()
What does this code do?
self.library.pop(index) removes the book at the given position and returns it. The method saves the updated list to the JSON file and shows which book was removed in the result label.
- Save book_picker.py and run the app again:
python3 book_picker.py
- Type a book title, author, and genre into the three input fields at the top.
- Click Add Book. You should see the new book appear in the library list below, and the fields clear automatically.
- Click the Remove button next to any book. The book disappears from the list and a confirmation message appears.
- Close and reopen the app. Your changes are still there because they were saved to library.json.
Books not saving between sessions?
Make sure save_library(self.library) is called in both add_book and remove_book. Without that line, changes only live in memory and vanish when the window closes.
Help me debug why my books aren't persisting
✔️ Awesome, I've got everything!
Great. Double check you've saved book_picker.py and that your app picks random books, adds new ones, and removes them correctly.
ⓧ I'd like to double check the full code
Here's the complete book_picker.py file. Compare it with yours to spot any differences.
import json
import random
import customtkinter
def load_library(filepath="library.json"):
"""Load the book library from a JSON file."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
def save_library(library, filepath="library.json"):
"""Save the book library to a JSON file."""
with open(filepath, "w", encoding="utf-8") as f:
json.dump(library, f, indent=4)
class BookPickerApp(customtkinter.CTk):
def __init__(self):
super().__init__()
self.title("Book Picker")
self.geometry("600x700")
self.library = load_library()
# Configure grid layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(3, weight=1)
# Title
self.title_label = customtkinter.CTkLabel(
self, text="Book Picker", font=("", 24, "bold")
)
self.title_label.grid(row=0, column=0, padx=20, pady=(20, 10))
# Add Book Section
self.add_frame = customtkinter.CTkFrame(self)
self.add_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
self.add_frame.grid_columnconfigure((0, 1, 2), weight=1)
self.title_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Title"
)
self.title_entry.grid(row=0, column=0, padx=5, pady=10, sticky="ew")
self.author_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Author"
)
self.author_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
self.genre_entry = customtkinter.CTkEntry(
self.add_frame, placeholder_text="Genre"
)
self.genre_entry.grid(row=0, column=2, padx=5, pady=10, sticky="ew")
self.add_button = customtkinter.CTkButton(
self.add_frame, text="Add Book", command=self.add_book
)
self.add_button.grid(row=1, column=0, columnspan=3, padx=5, pady=(0, 10))
# Pick Random Button
self.pick_button = customtkinter.CTkButton(
self, text="Pick a Random Book", command=self.pick_random_book,
font=("", 16, "bold"), height=40
)
self.pick_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
# Result Label
self.result_label = customtkinter.CTkLabel(
self, text="", font=("", 16), text_color=("#1f6aa5", "#5dade2")
)
self.result_label.grid(row=2, column=0, padx=20, pady=(60, 0))
# Book Library (Scrollable)
self.library_frame = customtkinter.CTkScrollableFrame(
self, label_text="Your Library"
)
self.library_frame.grid(row=3, column=0, padx=20, pady=(10, 20), sticky="nsew")
self.library_frame.grid_columnconfigure(0, weight=1)
self.refresh_library()
def refresh_library(self):
"""Clear and re-render the book list."""
for widget in self.library_frame.winfo_children():
widget.destroy()
if not self.library:
empty_label = customtkinter.CTkLabel(
self.library_frame, text="No books yet. Add some above!"
)
empty_label.grid(row=0, column=0, padx=10, pady=10)
return
for i, book in enumerate(self.library):
book_text = f"\"{ book['title']}\" by {book['author']} [{book['genre']}]"
book_label = customtkinter.CTkLabel(
self.library_frame, text=book_text, anchor="w"
)
book_label.grid(row=i, column=0, padx=10, pady=2, sticky="w")
remove_button = customtkinter.CTkButton(
self.library_frame, text="Remove", width=70,
fg_color="transparent", border_width=1,
command=lambda idx=i: self.remove_book(idx)
)
remove_button.grid(row=i, column=1, padx=5, pady=2)
def add_book(self):
"""Add a new book from the entry fields."""
title = self.title_entry.get().strip()
author = self.author_entry.get().strip()
genre = self.genre_entry.get().strip()
if not title or not author or not genre:
self.result_label.configure(text="Please fill in all fields.")
return
book = {"title": title, "author": author, "genre": genre}
self.library.append(book)
save_library(self.library)
# Clear entries
self.title_entry.delete(0, "end")
self.author_entry.delete(0, "end")
self.genre_entry.delete(0, "end")
self.result_label.configure(text=f"Added \"{title}\"!")
self.refresh_library()
def remove_book(self, index):
"""Remove a book by its index."""
removed = self.library.pop(index)
save_library(self.library)
self.result_label.configure(text=f"Removed \"{removed['title']}\"")
self.refresh_library()
def pick_random_book(self):
"""Pick a random book from the library."""
if not self.library:
self.result_label.configure(text="Library is empty! Add some books first.")
return
book = random.choice(self.library)
self.result_label.configure(
text=f"Read this: \"{book['title']}\" by {book['author']}"
)
if __name__ == "__main__":
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("blue")
app = BookPickerApp()
app.mainloop()
Your book picker is fully functional. You can add books, remove them, pick random recommendations, and everything persists between sessions. Next up: a secret mission to make your picks even smarter.
Secret mission
Filter by Genre
What if you could narrow down your random pick to a specific genre? Add a dropdown filter that lets you say 'pick me a random sci-fi book' instead of picking from the entire library.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- Your project folder on the Desktop (containing book_picker.py and library.json).
- The customtkinter Python package installed via pip3.
Keep everything running
No action needed. Choose this if you plan to keep building on your Book Picker app or want to reference the code later.
- Your project files stay on your Desktop where you left them.
- The customtkinter package remains installed and available for future Python GUI projects.
Pause - I'll come back to this later
Close any running processes to free up memory. Your files and packages stay intact for when you return.
- Close the Book Picker app window if it is still open.
- Close Cursor (or VS Code) if you are done for now.
Delete - I don't want to use this again
Remove all project files and the installed package to start fresh.
- Close the Book Picker app window and Cursor (or VS Code) if they are still open.
- Uninstall the customtkinter package by running this command in your terminal:
- Delete your project folder from the Desktop. Open Finder (macOS) or File Explorer (Windows), navigate to your Desktop, and move the project folder to the Trash or Recycle Bin.
pip3 uninstall customtkinter
That's a Wrap!
That's a Wrap!
Nice work! You've just built a polished desktop application that picks random books from your personal library, complete with a modern dark-mode interface and persistent data storage.
You've learned how to:
- Built a complete Python GUI application with CustomTkinter, including dark mode theming, rounded buttons, scrollable frames, and entry fields.
- Used random.choice() to make random selections from a list and Python's json module to persist data between sessions.
- Created an event-driven application with a class-based structure that responds to button clicks, manages state, and updates the interface dynamically.
- Added genre-based filtering with a dropdown menu for smarter random picks.
Ready to quiz yourself?