Skip to main content

How to Build an Inventory Management Desktop App with CustomTkinter?



In this tutorial, we will build a modern "Inventory Management System" using Python, Tkinter, and CustomTkinter. The application includes user login and signup functionality, image uploads for inventory items, form handling, and dashboard management. We will also explore how to display uploaded product images safely, manage UI state correctly, and avoid common Tkinter image issues such as the "pyimage error." By the end of this tutorial, you will have a clean and functional desktop inventory application with a modern user interface. 

Prerequisite:

This tutorial is part of the CustomTkinter Inventory Management System Series.

📚 View the Complete CustomTkinter Inventory Management System Series

⬅ Previous Part                                                                                                              ➡ 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 CTkDateEntry
The other dependencies are pre-installed with Python, including uuid, the image library, os, and shutil.

Then, we need to set up the file and folder structure, as below:
All the files and folders remain the same as in the previous tutorial, except that in this tutorial, I have added an uploads folder and a few images under the media folder to preload the default picture for the upload. I will work on both app.py and toplevel.py. 

Step 1: import the relevant libraries and set up the CTkfont and CTkimages
Before I begin, I have to import the libraries, including uuid, PIL, ctkdateentry, and so on. Next, I will set up the images and the font. Finally, I create and point out the uploads folder path, as shown in the code below:
import os
import shutil
from tkinter import filedialog
import customtkinter as ctk
from toplevel import open_login_window
from customtkinter import CTkImage
from PIL import Image
from ctkdateentry import CTkDateEntry, CTkStringVar
import uuid

root = ctk.CTk()
root.geometry("800x800")
root.title('Inventory Management System')
root.iconbitmap("inventory/media/icon.ico")
ctk.set_appearance_mode('dark') 
ctk.set_default_color_theme('inventory/custom_theme.json')

# Create a bold custom font for labels
font_bold = ctk.CTkFont(family="Roboto", size=20, weight="bold")

# Load the title image for the header section
login_title = CTkImage(
            dark_image=Image.open("inventory/media/title.png"),
            size=(700, 150))

# Load the default placeholder image for inventory items
# This image is shown before a user uploads a new picture
default_image = CTkImage(
            dark_image=Image.open("inventory/media/default.png"),
            size=(350, 200))

# Folder to store uploaded files 
UPLOAD_FOLDER = "inventory/uploads"
    
# Create folder if it doesn't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

Step 2: Create the header and body of the widgets.
Once the setup is ready, I will create the header and body of the app as follows:


My CustomTkinter widgets are listed below:
  1. Header                    - CTkLabel
  2. Log out                   - CTkButton
  3. Item Name             - CTkComboBox
  4. Vendor/Customer - CTkEntry
  5. Address                  - CTkTextbox
  6. Email                       - CTkEntry
  7. Reorder date          - CTkDateEntry (import from CTkDateEntry)
  8. selected date         - CTkLabel
  9. Stock status           - CTkRadioButton
  10. Quantity                  - CTkEntry
  11. Unit price                - CTkSlider
Here is my code:
# title image at the top of the dashboard
login_image = ctk.CTkLabel(root, image=login_title, text="") 
login_image.image = login_title 
login_image.grid(row=0, column=0, columnspan=4,
                 padx=10, pady=10, sticky='nsew')
                 
# log out button at the top right corner of the dashboard
log_out_button = ctk.CTkButton(root, text="Log Out", state='disabled', 
                               command=log_out)
log_out_button.grid(row=0, column=4,
                    padx=10, pady=20, sticky='e')
  
# Create a frame to hold all dashboard input fields and controls
dashboard_frame = ctk.CTkFrame(root, width=600, height=600, fg_color="grey")
dashboard_frame.grid(row=1, column=0, columnspan=5,
                     padx=10, pady=10, sticky="nsew")
                     
# Configure 5 columns in the dashboard frame
# Each column is given equal weight so they expand evenly
# when the application window is resized
for i in range(5):
    dashboard_frame.grid_columnconfigure(i, weight=1)
    
# Configure 8 rows in the dashboard frame
# Each row is also given equal weight for responsive resizing
for i in range(8):
    dashboard_frame.grid_rowconfigure(i, weight=1)

item_name_label = ctk.CTkLabel(dashboard_frame, text="Item Name:", 
                               font=font_bold)
item_name_label.grid(row=0, column=0, padx=10, pady=10)

# Placeholder function for combobox selection events
def combobox_callback(choice):
    pass

item_name_entry = ctk.CTkComboBox(dashboard_frame, 
                                  values=["Fruit", "Vegetable"],
                                  state='disabled',
                                  command=combobox_callback)
item_name_entry.grid(row=0, column=1, columnspan=4, padx=10, pady=10, 
                     sticky="ew")
item_name_entry.set("Select an item")

name_label = ctk.CTkLabel(dashboard_frame, text="Vendor/Customer:", 
                          font=font_bold)
name_label.grid(row=1, column=0, padx=10, pady=10)

name_entry = ctk.CTkEntry(dashboard_frame, 
                        state='disabled')
name_entry.grid(row=1, column=1, columnspan=4, padx=10, pady=10, sticky="ew")

address_label = ctk.CTkLabel(dashboard_frame, text="Address:", font=font_bold)
address_label.grid(row=2, column=0, padx=10, pady=10)

address_entry = ctk.CTkTextbox(dashboard_frame, 
                                height=100,
                                state='disabled')
address_entry.grid(row=2, column=1, columnspan=4, padx=10, pady=10, sticky="ew")

email_label = ctk.CTkLabel(dashboard_frame, text="Email:", font=font_bold)
email_label.grid(row=3, column=0, padx=10, pady=10)

email_entry = ctk.CTkEntry(dashboard_frame, 
                                 state='disabled')
email_entry.grid(row=3, column=1, columnspan=4, padx=10, pady=10, sticky="ew")

reorder_label = ctk.CTkLabel(dashboard_frame, text="Reorder Date:", font=font_bold)
reorder_label.grid(row=4, column=0, padx=10, pady=10)

# Use CTkStringVar to track changes in the CTkDateEntry widget
reorder_var = CTkStringVar(dashboard_frame)    
def select_date_event(*args):
    selected_date = reorder_var.get()
    select_date_label.configure(
        text=f"Selected date: {selected_date}"
    )
reorder_var.trace_add("write", select_date_event)

reorder_entry = CTkDateEntry(
    dashboard_frame,
    width=150,
    variable =reorder_var,
    justify ='left',
    font=('Roboto', 14, 'bold'))
reorder_entry.grid(row=4, column=1, padx=10, pady=10, sticky="w")

# Label to display the selected date from the CTkDateEntry widget
select_date_label = ctk.CTkLabel(dashboard_frame, text="No date selected.")
select_date_label.grid(row=5, column=1, columnspan=2, padx=10, pady=10, sticky="w")

stock_status_label = ctk.CTkLabel(dashboard_frame, text="Stock Status:", 
                                  font=font_bold)
stock_status_label.grid(row=6, column=0, padx=10, pady=10)

# Placeholder function for radio button events  
def radiobutton_event():
    pass

# Use StringVar to track the selected value of the radio buttons
radio_var = ctk.StringVar(value="1")
radiobutton_1 = ctk.CTkRadioButton(
    dashboard_frame,
    text="available for sales",
    command=radiobutton_event,
    variable=radio_var,
    value="1",
    state='disabled'
)
radiobutton_1.grid(row=6, column=1, sticky='w')

radiobutton_2 = ctk.CTkRadioButton(
    dashboard_frame,
    text="internal use only",
    command=radiobutton_event,
    variable=radio_var,
    value="2",
    state='disabled'
)
radiobutton_2.grid(row=7, column=1, sticky='w')

quantity_label = ctk.CTkLabel(dashboard_frame, text="Quantity:", font=font_bold)
quantity_label.grid(row=8, column=0, padx=10, pady=10)

quantity_entry = ctk.CTkEntry(dashboard_frame, width=100,
                                 state='disabled')      
quantity_entry.grid(row=8, column=1, padx=10, pady=10, sticky='w')

unit_price_label = ctk.CTkLabel(dashboard_frame, text="Unit Price:", font=font_bold)
unit_price_label.grid(row=9, column=0, padx=10, pady=10)

# Placeholder function for slider events
def slider_event(value):
    # Get current value from the CTkSlider
    price = unit_price_entry.get()

    # Format and display the price with 2 decimal places
    price_label.configure(text=f"{price:.2f}")

unit_price_entry = ctk.CTkSlider(dashboard_frame, from_=0, to=100, 
                                 command=slider_event, width=150,
                                 state='disabled')   
unit_price_entry.grid(row=9, column=1, padx=10, pady=10, sticky="w")

# Label to display the current price selected on the slider with 2 decimal places
price_label = ctk.CTkLabel(dashboard_frame, text="0.00")
price_label.grid(row=9, column=2, padx=10, pady=10,
    sticky="w")
Except for CTkDateEntry, the other widgets are disabled before users log in and become normal after the user is logged in.

The configuration of both rows and columns leaves sufficient space for adding a display of a picture, an upload button, and a label in the following step.


Step 3: Adding a display picture, an upload button and a label path
Now I am ready to create the following widgets:
  1. Display picture - CTkLabel
  2. Upload button  - CTkButton
  3. Upload path      - CTkLabel
Here is the code:
# Picture label for displaying the item image
picture_label = ctk.CTkLabel(dashboard_frame, image=default_image,
    width=350, height=200, fg_color="white")
picture_label.grid(row=4, column=3, columnspan=2, rowspan=4,
    padx=10, pady=10, sticky="nsew")
picture_label.image = default_image 

# Button to upload item picture
upload_button = ctk.CTkButton(dashboard_frame, text="Upload picture", 
                              state='disabled',
                              command=upload_picture)
upload_button.grid(row=8, column=3, padx=10, pady=10, sticky='e')

# Label to show upload status and filename of the uploaded picture
update_label = ctk.CTkLabel(dashboard_frame, text="No file uploaded yet.")
update_label.grid(row=8, column=4, padx=10, pady=10, sticky='w')
The display picture is loaded with a default picture from the media folder. If the user uploads a new picture, it will change to a new picture and show an upload path. 

Vice versa, if the user logs out, it will return the default picture and clear the upload path. 

However, I did not load any default picture. Once I log out, it will incur a "PyImage 7" error. 

What is "PyImage7" actually?

pyimage7 is

  • a temporary Tkinter internal image ID
  • created when I use CTkImage
  • stored in RAM only
  • tied to the widget and Tk instance
The picture label still wants to be used, and Tkinter has already deleted it from memory (when I logged out). Since the "PyImage7" is not available. Therefore, this error was incurred.

So, when the user uploads a picture, the code is shown as follows:
def upload_picture():
    # Open a file dialog to select image files
    file_path = filedialog.askopenfilename(
        filetypes=[("Images", "*.png *.jpg *.jpeg")]
    )
    # Continue only if a file was selected
    if file_path:
        # Get the file extension (e.g. .png, .jpg
        ext = os.path.splitext(file_path)[1]
        # Generate a unique filename using UUID
        # This prevents duplicate filenames
        filename = f"{uuid.uuid4().hex}{ext}"
      
        # Create the full destination path
        dest = os.path.join(UPLOAD_FOLDER, filename)
        # Copy the selected image into the uploads folder
        shutil.copy(file_path, dest)
        # Display the uploaded image in the picture label
        display_image(dest)
        # Create shorter display name
        display_name = filename[:15] + "..."
        
        # Update the status label with the uploaded filename
        update_label.configure(
            text=f"Uploaded:\n{display_name}",
            wraplength=180,
            justify="left"
        )
         
         
def display_image(image_path):
    # Open the image using Pillow
    img = Image.open(image_path)
    # Create a CTkImage for CustomTkinter
    # The image will be resized to 350x200 pixels
    new_image = ctk.CTkImage(
        light_image=img,
        dark_image=img,
        size=(350, 200)
    )
    # Display the image in the label
    # Remove the default text when the image is shown
    picture_label.configure(image=new_image, text="")
    # Keep a reference to prevent garbage collection
    # This avoids Tkinter pyimage errors
    picture_label.image = new_image
Here I have two functions. 
The first function focuses on the process of how the user uploads and stores the picture in the Upload folder and assigns a UUID number for each picture to avoid duplication of the picture with a similar file name. 

Meanwhile, the second function describes how to open, resize, and change the default picture to an uploaded picture and to display the uploaded picture in the label.


Step 4: Clear the dashboard data when logging out of the app
In the last tutorial, I created a simple log-out function. However, in this tutorial, I also added a few widgets as shown in Step 3 above. Therefore, now the logout function should include all those widgets.

I will set the following widgets' state as follows:
  1. log-out button          - disabled 
  2. item_name entry     - disabled 
  3. name_entry              - disabled 
  4. address_entry          - disabled 
  5. email_entry              - disabled 
  6. Stock_status_label - disabled
  7. radiobutton              - disabled 
  8. quantity_entry         - disabled 
  9. unit-price_entry       - disabled
  10. Update_label           - disabled
Before logging out of the app, I also need to delete all the relevant data, including reloading the default picture for the picture label.

Here is my code below:
def log_out():
    # Open the login window again after logging out
    open_login_window(root, log_out_button, item_name_entry, name_entry, 
                      address_entry, email_entry,
                      stock_status_label, radiobutton_1, radiobutton_2, 
                      quantity_entry, unit_price_entry,
                      update_label, select_date_label, price_label)
                      
    # Clear all dashboard fields and reset UI
    clear_dashboard()
    
    # Disable dashboard buttons and input fields
    # to prevent user interaction after logout
    log_out_button.configure(state='disabled')
    item_name_entry.configure(state='disabled')
    name_entry.configure(state='disabled')
    address_entry.configure(state='disabled')
    email_entry.configure(state='disabled')
    stock_status_label.configure(state='disabled')
    radiobutton_1.configure(state='disabled')
    radiobutton_2.configure(state='disabled')
    quantity_entry.configure(state='disabled')
    unit_price_entry.configure(state='disabled')
    update_label.configure(state='disabled')
    select_date_label.configure(state='disabled')
    price_label.configure(state='disabled')
    
   
def clear_dashboard():
    # Reset item selection dropdown
    item_name_entry.set("Select an item")

    # Clear text entry fields
    name_entry.delete(0, 'end')
    address_entry.delete("1.0", "end")
    email_entry.delete(0, 'end')

    # Reset reorder date and radio button selection
    reorder_var.set('')
    radio_var.set(0)

    # Clear quantity and reset unit price
    quantity_entry.delete(0, 'end')
    unit_price_entry.set(0)

    # Reset upload status label
    update_label.configure(text="No file uploaded yet.")
    select_date_label.configure(text="No date selected.")

    # Restore the default placeholder image
    # when the dashboard is cleared
    picture_label.configure(
        image=default_image,
        text="Picture:")

    # Keep a reference to the default image
    # to avoid Tkinter pyimage errors
    picture_label.image = default_image
    price_label.configure(text="0.00")

Step 5: When logged in to the app
After logging out, the user can log in again. All the code above is coded in the app.py file. The login logic is set up in the toplevel.py file. Therefore, I need to amend the log-in function both in the app.py and in the toplevel.py files. 

The logic for logging in to the toplevel.py file remains the same, except that in the previous tutorial, only the logout button was normal, but now all the widgets in step 4 above are set to normal too.

In addition, I also changed the function from destroy() to withdraw() in this tutorial.

The 'withdraw' function allows me to
  1.  Hide the window temporarily
  2.  Keep all widgets and variables alive
  3.  Reopen the same window later quickly
The amendment to the toplevel.py file
# Open the login window and pass dashboard widgets
# so their states can be controlled after login/logout
def open_login_window(root, log_out_button, item_name_entry, name_entry, 
                      address_entry, email_entry,
                      stock_status_label, radiobutton_1, radiobutton_2, 
                      quantity_entry, unit_price_entry,
                      upload_button, update_label,
                      select_date_label, price_label):

     def log_in():
        value = login_signup_var.get()
        email = login_email.get()
        password = login_password.get()
        confirm_password = confirm_password_entry.get()
        
        # Load existing users
        if os.path.exists('inventory/users.pkl'):
            with open('inventory/users.pkl', 'rb') as f:
                users = pickle.load(f)
        else:
            users = {}

        # ------------------ LOGIN ------------------
        if value == "Log In":
            if email in users and users[email] == password:
                CTkMessagebox(title="Success", message="Logged in successfully!", 
                   icon="check")
                   
                log_out_button.configure(state='normal')
                
                ##### This part will change #####
                # Enable dashboard buttons and input fields
                # to allow user interaction after login
                item_name_entry.configure(state='normal')
                name_entry.configure(state='normal')
                address_entry.configure(state='normal')
                email_entry.configure(state='normal')
                stock_status_label.configure(state='normal')
                radiobutton_1.configure(state='normal')
                radiobutton_2.configure(state='normal')
                quantity_entry.configure(state='normal')
                unit_price_entry.configure(state='normal')
                upload_button.configure(state='normal')
                update_label.configure(state='normal')
                select_date_label.configure(state='normal')
                price_label.configure(state='normal')
                
                # Hide the window temporarily
                toplevel.withdraw()
                ###### This part will change #####
return if email == '' or password == '': CTkMessagebox(title="Error", message="All fields are required!", icon="cancel") clear_fields() return else: CTkMessagebox(title="Error", message="Invalid email or password!", icon="cancel") clear_fields() return # ------------------ SIGN UP ------------------ else: if password != confirm_password: CTkMessagebox(title="Error", message="Passwords do not match!", icon="cancel") clear_fields() return if email in users: CTkMessagebox(title="Error", message="User already exists!", icon="cancel") clear_fields() return if email == '' or password == '' or confirm_password == '': CTkMessagebox(title="Error", message="All fields are required!", icon="cancel") clear_fields() return else: # Save new user users[email] = password with open('inventory/users.pkl', 'wb') as f: pickle.dump(users, f) clear_fields() CTkMessagebox(title="Success", message="Account created successfully!", icon="check") login_signup_var.set("Log In") segmented_button_event("Log In")

The state in the 'toplevel.py' file will take effect only if the function is also 'brought in' to app.py. Therefore, in app.py, I also need a necessary amendment, and below is my code:
# Import the login window function from the toplevel module
# This function is used to open the login/signup window
from toplevel import open_login_window
  
# Open the login window and pass all dashboard widgets
# so they can be enabled or disabled after login/logout
open_login_window(root, log_out_button, item_name_entry, 
                  name_entry, address_entry, email_entry,
                  stock_status_label, radiobutton_1, radiobutton_2, 
                  quantity_entry, unit_price_entry,
                  upload_button, update_label,
                  select_date_label, price_label)
Here is the demo of the app

Final Wrap-up: 
In this tutorial, we built a modern "Inventory Management System" using Python with CustomTkinter. The application includes a secure login and signup system, a responsive dashboard interface, and full CRUD-style input handling for inventory items. Users can add product details, manage stock information, and upload images for each item with a clean preview system. The app also supports persistent file storage using UUID-based image naming to prevent conflicts and ensure reliable file management. With a structured UI, dynamic form controls, and safe image handling, this project demonstrates how to build a practical desktop application with a professional and user-friendly interface.

Published: May 2026
Last Updated: May 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                                                                                                         ...