Welcome to this tutorial, where we’ll build a modern inventory management system using Python and CustomTkinter. You’ll learn how to create a clean, user-friendly interface with a functional login and signup system. This project focuses on implementing full CRUD operations in a real-world application. By the end, you’ll have a complete, scalable desktop app ready for further enhancement.
Prerequisite:
This tutorial is part of the CustomTkinter Inventory Management System Series.
📚 View the Complete CustomTkinter Inventory Management System Series
PreliminaryBefore I begin, please activate the virtual environment and install the required dependencies.
python -m venv venv
venv\Scripts\activate
pip install customtkinter CTkMessagebox
Since the Python Image Library (PIL), os, and pickle are standard libraries, they are preinstalled. Then, we need to set up the file and folder structure, as below:
There are 2 Python files: the dashboard window is app.py, and the subsidiary window is the login/signup window (toplevel.py), as shown in the diagram above. The media file contains the title image, illustration image, and button icons. The custom_theme.json is a file where I want to define how each widget is displayed according to my preference, and you can define your preference too.
The dashboard window is where a CRUDS (create, read, update, delete, and search) operation is performed. However, for this tutorial, I need to create a dashboard title and a log-out button in the diagram below.
# Import CustomTkinter library
import customtkinter as ctk
# Create the dashboard application window
root = ctk.CTk()
# Set window size (width x height)
root.geometry("800x800")
# Set window title
root.title('Inventory Management System')
# Set appearance mode (light/dark/system)
ctk.set_appearance_mode('light')
# Load custom theme (JSON file for colors/styles)
ctk.set_default_color_theme('inventory/custom_theme.json')
# Function for logout button (currently empty)
def log_out():
pass
# Create a label for the dashboard title
dashboard_label = ctk.CTkLabel(root, text="Dashboard",
font=ctk.CTkFont(size=30, weight="bold"))
dashboard_label.pack(pady=20)
# Create a logout button (disabled by default)
log_out_button = ctk.CTkButton(root, text="Log Out",
state='disabled', command=log_out)
log_out_button.pack(pady=10)
# Start the GUI event loop (keeps the window running)
root.mainloop()The log-out function will be created after I have set up the subsidiary window; therefore, I will create it later on. Step 2: Set up the subsidiary login/signup window
After the dashboard window is ready, I go ahead to set up the subsidiary window. The subsidiary window contains both the log-in page and the sign-up page.
The above-left diagram is a log-in page; meanwhile, the right diagram is a sign-up page. To log in, the user must first sign up with an email, a password, and a confirmed password. For simplicity, I use pickle as an object to serialise the users' information for both reading and writing.
Why is Pickle preferred?
1) Pickle can serialise almost every commonly used built-in Python data type. It also retains the exact state of the object.
2) Pickle is also a suitable choice when storing recursive structures since it only writes an object once.
3) Pickle allows for flexibility when deserialising objects. You can easily save different variables into a Pickle file and load them back in a different Python session, recovering your data exactly the way it was without having to edit your code.
Next, let us dive into my code in toplevel.py
The above subsidiary window (toplevel.py) setup will not run unless it is imported to the dashboard window (app.py) as shown below.
import customtkinter as ctk
from PIL import Image
from customtkinter import CTkImage
# Function to open the login/signup window
def open_login_window(root, log_out_button):
# Create a new top-level window (child window of root)
toplevel = ctk.CTkToplevel(root)
# Set window size and title
toplevel.geometry("500x580")
toplevel.title("Login")
# Define a bold font style
font_bold = ctk.CTkFont(family="Roboto", size=20, weight="bold")
# ------------------ LOAD IMAGES ------------------
login_image = CTkImage(
dark_image=Image.open("inventory/media/coding.png"),
size=(150, 150)
)
login_title = CTkImage(
dark_image=Image.open("inventory/media/title.png"),
size=(500, 100)
)
login_icon = CTkImage(
dark_image=Image.open("inventory/media/login.png"),
size=(30, 30))
signup_icon = CTkImage(
dark_image=Image.open("inventory/media/signup.png"),
size=(30, 30))
# ------------------ SEGMENTED BUTTON (LOGIN / SIGNUP) ------------------
# Variable to track selected mode (Log In / Sign Up)
login_signup_var = ctk.StringVar(value="Log In")
# Segmented toggle button
login_signup = ctk.CTkSegmentedButton(
toplevel,
values=["Log In", "Sign Up"],
variable=login_signup_var,
command=segmented_button_event)
login_signup.pack(padx=10, pady=10)
# ------------------ DISPLAY IMAGES ------------------
# Display illustration image
login_image = ctk.CTkLabel(toplevel, image=login_image, text="")
login_image.pack(padx=0, pady=0)
# Display title image
login_title = ctk.CTkLabel(toplevel, image=login_title, text="")
login_title.pack(padx=0, pady=0)
# ------------------ LOGIN FORM FRAME ------------------
# Container frame for form elements
login_frame = ctk.CTkFrame(toplevel)
login_frame.pack(padx=10, pady=10)
# Heading text
login_heading= ctk.CTkLabel(login_frame, text="Login to your account",
font=font_bold)
login_heading.grid(column=0, row=0, columnspan=2, padx=10, pady=10)
# ------------------ EMAIL FIELD ------------------
login_label_1 = ctk.CTkLabel(login_frame, text="Email:",
font=font_bold)
login_label_1.grid(column=0, row=1, padx=10, pady=10)
login_email = ctk.CTkEntry(login_frame,
placeholder_text="Enter your email address",
width=200)
login_email.grid(column=1, row=1, padx=10, pady=10)
# ------------------ PASSWORD FIELD ------------------
login_label_2 = ctk.CTkLabel(login_frame, text="Password:",
font=ctk.CTkFont(size=20, weight="bold"))
login_label_2.grid(column=0, row=2, padx=10, pady=10)
login_password = ctk.CTkEntry(login_frame,
placeholder_text="Enter your password",
width=200,show="*")
login_password.grid(column=1, row=2, padx=10, pady=10)
# ------------------ CONFIRM PASSWORD (FOR SIGNUP) ------------------
confirm_password_label = ctk.CTkLabel(login_frame, text="Confirm Password:",
font=ctk.CTkFont(size=20, weight="bold"))
confirm_password_label.grid_forget()
confirm_password_entry = ctk.CTkEntry(login_frame,
placeholder_text="Confirm your password",
width=200,
show="*")
confirm_password_entry.grid_forget()
# ------------------ SHOW PASSWORD CHECKBOX ------------------
show = ctk.CTkCheckBox(login_frame, text="Show Password",
command=show_password)
show.grid(column=0, row=3, columnspan=2, padx=10, pady=10)
# ------------------ LOGIN BUTTON ------------------
login_button = ctk.CTkButton(login_frame, text="Log In",
image=login_icon,
compound="left",
font=font_bold,
width=200,
command=log_in)
login_button.grid(column=0, row=4, columnspan=2, padx=10, pady=10)
# Return the toplevel window (optional use outside)
return toplevelYou may ask, "Why pass
log_out_button as an argument to open_login_window?"By default, the state of the log-out button is disabled. By passing it, it will enable the state of the logout button in the dashboard window once the user has successfully logged in.
The confirm password field (grid_forget) is hidden when the app is running; it will appear only if the user clicks the sign-up segment button.
# imported from the toplevel.py
from toplevel import open_login_window
# load the subsidiary window when the app is running
open_login_window(root, log_out_button)Even though I have set up the segmented button for the log-in and sign-up buttons (value), the login page is set as the default. To log in, I need to fill out the email, password, and confirm password fields and click the sign-up button below. Once I have signed up, I will be redirected back to the login page. Then, I need to refill the same email and password fields and click the log-in button to authorise a user's access to the dashboard window.
The code is as follows:
def segmented_button_event(value):
# This function is triggered when the segmented button changes
# 'value' will be either "Log In" or "Sign Up"
if value == "Log In":
# ------------------ LOGIN MODE ------------------
# Change heading text to login
login_heading.configure(text="Login to your account")
# Hide confirm password fields (not needed for login)
confirm_password_label.grid_forget()
confirm_password_entry.grid_forget()
# Show "Show Password" checkbox in row 3
show.grid(column=0, row=3, columnspan=2, padx=10, pady=10)
# Change button text and icon to "Log In"
login_button.configure(text="Log In", image=login_icon)
else:
# ------------------ SIGN UP MODE ------------------
# Change heading text to signup
login_heading.configure(text="Create a new account")
# Show confirm password fields (needed for signup)
confirm_password_label.grid(column=0, row=3, padx=10, pady=10)
confirm_password_entry.grid(column=1, row=3, padx=10, pady=10)
# Hide "Show Password" checkbox
show.grid_forget()
# Change button text and icon to "Sign Up"
login_button.configure(text="Sign Up", image=signup_icon)
# ------------------ SEGMENTED BUTTON (LOGIN / SIGNUP) ------------------
# Variable to track selected mode (Log In / Sign Up)
login_signup_var = ctk.StringVar(value="Log In")
# Segmented toggle button
login_signup = ctk.CTkSegmentedButton(
toplevel,
values=["Log In", "Sign Up"],
variable=login_signup_var,
command=segmented_button_event)
login_signup.pack(padx=10, pady=10)
When I click the log-in segment button: (a) The heading text is "Login to your account",
(b) Only the email and password fields are available,
(c) The show password checkbox is displayed, and
(d) A log-in button.
On the one hand, when I click the sign-up segment button:
(a) The heading text changes to "Create a new account",
(b) A new confirmation password will be displayed,
(c) The show password checkbox will be hidden, and
(d) The log-in button changed to a sign-up button.
I also set up a show-or-hide checkbox password for users who seek protection for their privacy, as shown in the diagram below:
When the button is checked (value = 1), show the password input; otherwise (value = 0), hide the password with '***' characters.Let us look at the code below:
def show_password():
# Get the current state of the checkbox
# 1 = checked, 0 = unchecked
show_state = show.get()
if show_state == 1:
# If the checkbox is checked, show the password (no masking)
login_password.configure(show="")
else:
# If the checkbox is unchecked, hide the password with "*"
login_password.configure(show="*")
show = ctk.CTkCheckBox(login_frame, text="Show Password",
command=show_password)
show.grid(column=0, row=3, columnspan=2, padx=10, pady=10)
Step 5: Validate the 'Log-In' and 'Sign-Up' buttons
When you click the sign-up button, it will validate the user information from the pickle file to determine whether the email and password exist or if there are incomplete input fields, and then it will prompt a message to inform the user regarding the status of the signing up. Clicking the log-in button performs the same validation to confirm if the email and password match the pickle file and then prompts a message about the login status. Here is the code:
Step 6: Configure the log-out button
def log_in():
# Get current mode (Log In or Sign Up)
value = login_signup_var.get()
# Get user inputs from entry fields
email = login_email.get()
password = login_password.get()
confirm_password = confirm_password_entry.get()
# ------------------ LOAD EXISTING USERS ------------------
# Check if user data file exists
if os.path.exists('inventory/users.pkl'):
# Load existing users from pickle file
with open('inventory/users.pkl', 'rb') as f:
users = pickle.load(f)
else:
# If file doesn't exist, create empty user dictionary
users = {}
# ------------------ LOGIN ------------------
if value == "Log In"
# Check if email exists and password matches
if email in users and users[email] == password:
CTkMessagebox(title="Success", message="Logged in successfully!",
icon="check")
# Close login window
toplevel.destroy()
# Enable logout button in dashboard
log_out_button.configure(state='normal')
return
# Check if any required field is empty
if email == '' or password == '':
CTkMessagebox(title="Error", message="All fields are required!",
icon="cancel")
clear_fields()
return
else:
# Invalid login credentials
CTkMessagebox(title="Error", message="Invalid email or password!",
icon="cancel")
clear_fields()
return
# ------------------ SIGN UP ------------------
else:
# Check if passwords match
if password != confirm_password:
CTkMessagebox(title="Error", message="Passwords do not match!",
icon="cancel")
clear_fields()
return
# Check if user already exists
if email in users:
CTkMessagebox(title="Error", message="User already exists!",
icon="cancel")
clear_fields()
return
# Check if any field is empty
if email == '' or password == '' or confirm_password == '':
CTkMessagebox(title="Error", message="All fields are required!",
icon="cancel")
clear_fields()
return
else:
# Add new user to dictionary
users[email] = password
# Save updated users back to pickle file
with open('inventory/users.pkl', 'wb') as f:
pickle.dump(users, f)
clear_fields()
# Show success message
CTkMessagebox(title="Success", message="Account created successfully!",
icon="check")
# Switch UI back to login mode
login_signup_var.set("Log In")
segmented_button_event("Log In")
def clear_fields():
# Clear the email input field
# Deletes text from position 0 to the end
login_email.delete(0, 'end')
# Clear the password input field
login_password.delete(0, 'end')
# Clear the confirm password field (used in Sign Up)
confirm_password_entry.delete(0, 'end')When the user clicks the log-out button, it will regenerate the log-in/sign-up window as in step 2 above and set the log-out button in the dashboard window to disabled. So when I clicked the log-out button, nothing happened, since the button was no longer clickable.
The code below explains what the dashboard window (app.py) does:
Final wrap-up:
# Import the function that opens the login window
from toplevel import open_login_window
# Function to handle logout action
def log_out():
# Open the login window again when the user logs out
open_login_window(root, log_out_button)
# Disable the logout button (since user is no longer logged in)
log_out_button.configure(state='disabled')
# Automatically open the login window when the app starts
open_login_window(root, log_out_button)This implementation is a simple illustration of a login and signup system built with CustomTkinter, where user information is stored locally using Python’s pickle module for basic persistence. It is intended for learning and demonstration purposes, showing how user input, UI interaction, and data storage can be connected in a lightweight way without a database. In a more advanced and production-ready version, this approach would be replaced with a proper database solution such as SQLAlchemy, which provides better data management, security, scalability, and integration with relational databases like PostgreSQL or MySQL.
Published: Mar 2026
Last Updated: Mar 2026
About the Author
Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.







Comments
Post a Comment