Anyone can put a button on screen. These principles separate a toy script from a robust, responsive, distributable application.
As an app grows, cramming everything into one file becomes unmaintainable. Adopt a structure that separates concerns. The classic choice for GUIs is a variant of MVC / MVP / MVVM:
Pure data & business logic. Knows nothing about widgets. Easy to unit-test.
The widgets. Displays state and forwards user events. Contains no business rules.
The glue: reacts to events, updates the model, refreshes the view.
When logic lives outside the UI you can test it without launching a window, swap toolkits later, and reason about bugs far more easily.
A callback should be a thin translator: read inputs, call a real function, show the result. Don't bury algorithms inside event handlers.
def on_click():
# business logic mixed into the UI
total = 0
for line in open("data.csv"):
total += float(line.split(",")[2])
label.config(text=total)
def compute_total(path): # testable, no UI
return sum(float(l.split(",")[2])
for l in open(path))
def on_click(): # thin translator
label.config(text=compute_total("data.csv"))
This is the #1 cause of "(Not Responding)" windows. Any task longer than ~100 ms β network calls, file crunching, heavy computation β must run off the main thread. Crucially, most toolkits require that widgets only be touched from the main thread, so the worker must hand results back safely.
import threading, queue, time
import tkinter as tk
from tkinter import ttk
results = queue.Queue()
def heavy_work():
time.sleep(3) # pretend this is slow I/O
results.put("Done!") # push result to the queue
def start():
status.config(text="Workingβ¦")
threading.Thread(target=heavy_work, daemon=True).start()
root.after(100, poll) # check queue on the main thread
def poll():
try:
status.config(text=results.get_nowait())
except queue.Empty:
root.after(100, poll) # not ready yet, check again
root = tk.Tk()
status = ttk.Label(root, text="Idle"); status.pack(padx=20, pady=10)
ttk.Button(root, text="Start", command=start).pack(pady=8)
root.mainloop()
In Qt use a QThread/QRunnable worker that emits a
signal when finished; the connected slot runs safely on the GUI thread.
For async I/O, libraries like qasync integrate asyncio with the
Qt loop.
StringVar, etc.) or Qt's model/view classes to
keep data and display in sync automatically.update_ui() function that refreshes widgets from state,
so you have one place to reason about what the screen shows.Every action needs a visible reaction β a status message, spinner, progress bar or disabled button. Silence feels broken.
Disable invalid actions, validate input as it's typed, and use sensible defaults instead of relying on error messages.
Reuse spacing, fonts, colours and button placement. Consistency lets users predict behaviour.
Confirm destructive actions, support undo, remember window size and recent files, and never lose unsaved work silently.
Provide shortcuts and a logical tab order. Power users rarely reach for the mouse.
Use whitespace and alignment to create visual hierarchy. Related controls belong together in a frame.
An unhandled exception in a callback can crash or silently kill part of the UI. Wrap risky operations and tell the user what happened in plain language.
from tkinter import messagebox
def save():
try:
write_file(data)
except PermissionError:
messagebox.showerror("Cannot save",
"The file is read-only or open elsewhere.")
except Exception as exc:
messagebox.showerror("Unexpected error", str(exc))
log.exception("save failed") # keep a real log too
End users don't have Python installed. Bundle your app into a single executable or installer so they can just double-click it.
| Tool | What it produces | Notes |
|---|---|---|
| PyInstaller | Standalone .exe / app bundle | Most popular; cross-platform; one-file mode |
| cx_Freeze | Frozen executable | Mature, good Windows/Linux support |
| Nuitka | Compiled C executable | Can improve performance & obfuscation |
| briefcase (BeeWare) | Native installers, incl. mobile | Pairs well with Toga |
| Buildozer | Android APK / iOS | For Kivy mobile apps |
pip install pyinstaller
pyinstaller --onefile --windowed --name MyApp main.py
# --windowed : no console window for GUI apps
# --onefile : a single distributable executable
# result lands in the dist/ folder
Images, icons and data files must be explicitly included (e.g. --add-data)
and referenced via paths that work after freezing β use sys._MEIPASS or
importlib.resources rather than relative paths.
Useful guides and tools (from webpage_links.xlsx).