Skip to main content

How to Build a Secure Python & CustomTkinter Login System?

Inventory Management System

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

                                                                                                                                             ➡ Next Part

Preliminary
Before 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:
toplevel
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.

Step 1: Set up the dashboard window
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.

main window

Therefore, my code is as follows:
# 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. 

Log InSign Up
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
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 toplevel
You 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.

The above subsidiary window (toplevel.py) setup will not run unless it is imported to the dashboard window (app.py) as shown below.
# 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)

Step 3: Navigate the subsidiary login/signup window
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.


Step 4: Set up the show/hide checkbox
I also set up a show-or-hide checkbox password for users who seek protection for their privacy, as shown in the diagram below:
Show/hide password

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:
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')

Step 6: Configure the log-out button
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.
disabled
The code below explains what the dashboard window (app.py) does:
# 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)

Final wrap-up:
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                                                                                                                            
---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 Get Your FREE Flask Cheat Sheet

Join 500+ Python learners and receive this printable Flask Cheat Sheet instantly. Plus, get new Flask tutorials delivered to your inbox.

✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates

-----------------------------------------------------------------------------------------------------------------------------
Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 


-----------------------------------------------------------------------------------------------------------------------------

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

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.