Skip to main content

How to Implement a Secure Login and Sign-Up System in Python & CustomTkinter?

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                                                                                                                            

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 Design a Location Tracking Module for Desktop Business Systems Using Python?

This tutorial will show you how to create an interactive map application that allows users to input geographic coordinates and visualize locations on an interactive map using Python's Tkinter GUI framework enhanced with TTKBootstrap styling and the TkinterMapView widget.  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 ttkbootstrap tkintermapview As usual, we need to import the relevant module and set the root values from tkinter import * import ttkbootstrap as tb import tkintermapview from ttkbootstrap.dialogs import Messagebox root = tb.Window(themename='darkly') root.title('Find My Map') root.geometry("1200x1200") I would like to divide it into three sections.  a) Input Panel Layout,  The headers are mainly a label...

How to Create Flask Forms with CKEditor and Flask-WTF?

In this tutorial, we will be integrating Flask-WTF and Flask-CKEditor into your Flask application! You'll learn how to set up the editor, securely handle formatted HTML content, and create a seamless user experience that enhances any project that requires user-generated content. Let's get started! Prerequisite: This tutorial is part of the Flask CKEditor Project Series. 📚 View the Complete Flask CKEditor Series                                                                                                                                                  ➡ Next Part Preliminary Before I begin, it is recommended to activate the v...

How to Store Application Data with SQLite in Python?

  In the previous tutorial, I used TinyDB as our storage solution to keep application data in a simple JSON-based format. While TinyDB is lightweight and easy to use, many web applications require a more structured and scalable database system. In this tutorial, we will switch to SQLite , a powerful relational database that integrates well with Python and web frameworks. By using SQLite, you will learn how to store, manage, and query data in a more structured way for real-world applications. Prerequisite: This tutorial is part of the Flask CKEditor Project Series. 📚 View the Complete Flask CKEditor Series ⬅ Previous Part                                                                                                         ...