Building Modern Desktop Applications with Python and CustomTkinter

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Have you ever been embarrassed by how your Python app looks?

I once built an internal tool for my team — the functionality was solid, but the interface looked like something from the Windows XP era. Gray windows, boxy buttons, tiny fonts. A colleague glanced at it and asked: “What did you build this with? It looks like Notepad.”

That was the moment I started looking for a way to make Python apps look more professional without having to learn an entirely new framework.

The Problem with Traditional Tkinter

Tkinter is the GUI library that ships with Python — no extra installation needed, runs on Windows, macOS, and Linux. But its biggest drawback is that the default interface looks very dated. Widgets have no rounded corners, no dark mode, and follow no modern design language whatsoever.

Many developers switch to PyQt5 or PySide6 — far more powerful, but they come with a steep learning curve: Qt Designer, signals/slots, and PyQt5’s licensing is a real headache for commercial projects.

What about wxPython? Similar story — hard to get started, sparse documentation, and a significantly smaller community.

So if you just need a simple internal tool that actually looks decent, is there a lighter-weight option?

CustomTkinter — Tkinter Rebuilt for 2024

CustomTkinter is a Python library developed by Tom Schimansky. At its core, it wraps Tkinter and replaces all widgets with redrawn versions — rounded corners, modern colors, and automatic dark/light mode that follows the operating system.

The best part: if you already know Tkinter, there’s almost nothing new to learn. The syntax is nearly identical — just add the ctk prefix to widget names and you’re done.

Installation

pip install customtkinter

No heavy dependencies. No Qt, no C++ compiler.

Building Your First App — Step by Step

Let’s use a real-world example: a temperature unit converter (Celsius ↔ Fahrenheit). Small, but enough to see how CustomTkinter works.

Basic Skeleton

import customtkinter as ctk

# Set default appearance
ctk.set_appearance_mode("System")   # Follow OS setting: Light/Dark
ctk.set_default_color_theme("blue") # Primary color theme: blue, green, dark-blue

app = ctk.CTk()                     # Main window (replaces tk.Tk())
app.title("Temperature Converter")
app.geometry("400x300")

app.mainloop()

Run it and you immediately get a clean white window with rounded corners that looks like a macOS app. The difference is obvious from the very first line.

Adding Widgets and Logic

import customtkinter as ctk

ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")

def convert():
    try:
        celsius = float(entry_celsius.get())
        fahrenheit = celsius * 9 / 5 + 32
        label_result.configure(text=f"{fahrenheit:.1f} °F")
    except ValueError:
        label_result.configure(text="Please enter a valid number!")

app = ctk.CTk()
app.title("Temperature Converter")
app.geometry("400x280")
app.resizable(False, False)

# Title label
ctk.CTkLabel(app, text="Temperature Converter",
             font=ctk.CTkFont(size=20, weight="bold")).pack(pady=20)

# Input field
entry_celsius = ctk.CTkEntry(app, placeholder_text="Enter °C...", width=200)
entry_celsius.pack(pady=10)

# Convert button
ctk.CTkButton(app, text="Convert to °F", command=convert, width=200).pack(pady=10)

# Result label
label_result = ctk.CTkLabel(app, text="", font=ctk.CTkFont(size=18))
label_result.pack(pady=10)

# Toggle dark/light mode
def toggle_mode():
    current = ctk.get_appearance_mode()
    ctk.set_appearance_mode("Light" if current == "Dark" else "Dark")

ctk.CTkButton(app, text="Toggle Dark/Light", command=toggle_mode,
              fg_color="gray40", hover_color="gray30", width=140).pack(pady=5)

app.mainloop()

Under 35 lines of code, and you already have a decent-looking app with a dark mode toggle. And it’s not fake dark mode — CustomTkinter fully redraws every widget, so the dark background is the real deal.

The Most Useful Widgets

CustomTkinter covers all the common widgets. Here are the ones I use most often in internal tools:

  • CTkEntry — text input field with placeholder support
  • CTkButton — button with customizable fg_color and hover_color
  • CTkLabel — text label with custom font support
  • CTkFrame — container frame for grouping and laying out widgets
  • CTkTextbox — multi-line text editor (replaces Text)
  • CTkComboBox — dropdown selector
  • CTkSwitch — iOS-style toggle switch
  • CTkProgressBar — progress bar
  • CTkScrollableFrame — scrollable frame, great for long lists
  • CTkTabview — tab layout, splits the screen into multiple tabs

Real-World Layout with Multiple Tabs

Need something a bit more complex? I often use CTkTabview to organize content instead of cramming everything onto one screen:

import customtkinter as ctk

app = ctk.CTk()
app.geometry("500x350")
app.title("Internal Tool")

tabview = ctk.CTkTabview(app, width=480, height=320)
tabview.pack(padx=10, pady=10)

tabview.add("File Processing")
tabview.add("Settings")
tabview.add("Log")

# Tab 1 content
ctk.CTkLabel(tabview.tab("File Processing"),
             text="Drop files here...").pack(pady=40)

# Tab 3 content — textbox for displaying logs
log_box = ctk.CTkTextbox(tabview.tab("Log"), width=440, height=200)
log_box.pack(padx=10, pady=10)
log_box.insert("end", "[INFO] App started...\n")

app.mainloop()

A Quick Tip When Building Text Processing Tools

When I was building a log processing tool, I had a section that required validating regex patterns before putting them into Python code. Instead of running the script over and over, I’d quickly test on toolcraft.app/en/tools/developer/regex-tester — runs right in the browser, nothing to install, results update in real time. Once the pattern looked good, I’d paste it into the CustomTkinter code. Saved a lot of debugging time.

Packaging into an .exe to Share

Once your app is done, want to send it to colleagues without requiring them to install Python? Use PyInstaller:

# Install PyInstaller
pip install pyinstaller

# Package into a single .exe file (Windows)
pyinstaller --onefile --windowed --name "AppName" main.py

# Output is in the dist/ folder
# dist/AppName.exe

The --windowed flag hides the terminal window when running (only the GUI window appears). --onefile bundles everything into a single file, making it easier to share.

One thing to know upfront: executables packaged with CustomTkinter typically run around 30–50MB. A bit large for a small app, but nobody complains when it’s an internal tool.

Is CustomTkinter Suitable for Large Projects?

Honest answer: not really. If you need a complex desktop application — multiple child windows, advanced drag-and-drop, OpenGL, or mobile builds — PyQt/PySide6 or Kivy will serve you better.

But if you need:

  • An internal tool for a small team (1–10 users)
  • An automation script with a GUI instead of a command line
  • A quick prototype for a demo
  • A small app that still needs to look polished

…then CustomTkinter is the best choice — fast to learn, minimal code, great results.

Wrapping Up

CustomTkinter fills exactly the gap that Tkinter has left open for years: an outdated interface. It doesn’t try to do everything — it just does one thing well. And if you already know Tkinter, there’s virtually nothing to relearn.

Next time you need to build an internal tool, instead of writing a command-line script and then explaining how to use it to the whole team, try spending an extra hour or two building a UI with CustomTkinter — your colleagues will stop looking at you with that skeptical expression.

Share: