All posts

Writing · python / tkinter / automation

Building a Desktop Tool Your Colleagues Can Use with Python's tkinter

Turn a data script into a small tkinter app: ttk widgets and grid, variable-bound state, a responsive main loop and logic kept out of the GUI.

Author
Catalin Bondari
Published
Reading time
5 min

Most data work ends up as a script that only its author can run. That is fine until a colleague needs to run it too, and “open a terminal, activate the environment, pass the file path as an argument” turns a two-minute job into a support call. A small desktop window with a Browse button and a results table often solves the problem, and Python ships with everything needed to build one: tkinter.

Plenty of tutorials cover the “hello world” window. This post covers the next step: a tool someone else can actually use. The example is a CSV checker for a holdings-style file. It flags malformed identifiers, duplicates and bad prices, and shows the problems in a table. Along the way it uses the four things I think separate a usable Tkinter app from a demo.

1. Use ttk widgets and the grid manager

Tkinter has two widget sets. The classic ones look dated on most systems. The themed widgets in tkinter.ttk pick up the native look of Windows and macOS, and they include widgets the classic set lacks, such as Treeview for tables. Import both and default to ttk.

For layout, use grid rather than pack once a window has more than a couple of widgets. It thinks in rows and columns, which maps directly onto a form. The detail most people miss is columnconfigure and rowconfigure with a weight: without them, nothing stretches when the user resizes the window.

import tkinter as tk
from tkinter import ttk, filedialog, messagebox

class App(ttk.Frame):
    def __init__(self, master):
        super().__init__(master, padding=12)
        self.grid(sticky="nsew")
        master.columnconfigure(0, weight=1)
        master.rowconfigure(0, weight=1)

        self.path = tk.StringVar()
        self.status = tk.StringVar(value="Choose a CSV file to check.")

        ttk.Label(self, text="File").grid(row=0, column=0, sticky="w")
        ttk.Entry(self, textvariable=self.path, width=50).grid(
            row=0, column=1, sticky="ew", padx=6)
        ttk.Button(self, text="Browse", command=self.browse).grid(row=0, column=2)
        self.run_btn = ttk.Button(self, text="Run checks", command=self.run)
        self.run_btn.grid(row=1, column=2, pady=8, sticky="e")

        self.tree = ttk.Treeview(self, columns=("row", "problem"),
                                 show="headings", height=12)
        self.tree.heading("row", text="Row")
        self.tree.heading("problem", text="Problem")
        self.tree.grid(row=2, column=0, columnspan=3, sticky="nsew")
        ttk.Label(self, textvariable=self.status).grid(
            row=3, column=0, columnspan=3, sticky="w", pady=(8, 0))

        self.columnconfigure(1, weight=1)   # the entry stretches sideways
        self.rowconfigure(2, weight=1)      # the table stretches downwards

Wrapping the window in a class that subclasses ttk.Frame keeps state, such as the selected path, on self instead of in global variables. That matters as soon as the app grows a second button.

2. Bind state to variables, not to widgets

StringVar, IntVar and BooleanVar hold a value that one or more widgets display. Above, the entry and the status label are both bound to variables, so the code updates self.status.set(...) and never needs to reach into a label to change its text. The file dialog writes to the same variable the entry shows, so typing a path and browsing for one end up in the same place:

    def browse(self):
        chosen = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
        if chosen:
            self.path.set(chosen)

filedialog and messagebox give you native open/save and alert dialogs for free; the Python docs list them under Tkinter dialogs.

3. Never block the main loop

This is the one that catches everyone. Tkinter runs a single event loop. If a button’s callback reads a 200 MB file, the window freezes until it finishes, and on some systems the operating system offers to kill it. The fix is to do the work on a background thread and send results back through a queue, because Tkinter widgets should only be touched from the main thread.

The main thread then polls the queue with after, which schedules a function to run after a delay without blocking anything:

    def run(self):
        if not self.path.get():
            messagebox.showwarning("No file", "Choose a file first.")
            return
        self.tree.delete(*self.tree.get_children())
        self.run_btn.state(["disabled"])
        self.status.set("Checking...")
        threading.Thread(target=self.worker, args=(self.path.get(),),
                         daemon=True).start()
        self.after(100, self.poll)

    def worker(self, path):
        # Background thread: only talks to the queue, never to widgets.
        try:
            for item in check_file(path):
                self.results.put(item)
        except OSError as exc:
            self.results.put((0, f"Could not read file: {exc}"))
        self.results.put(None)          # sentinel: finished

    def poll(self):
        while True:
            try:
                item = self.results.get_nowait()
            except queue.Empty:
                self.after(100, self.poll)   # nothing yet, check again soon
                return
            if item is None:
                count = len(self.tree.get_children())
                self.status.set(f"Done. {count} problem(s) found.")
                self.run_btn.state(["!disabled"])
                return
            self.tree.insert("", "end", values=item)

Here self.results is a queue.Queue() created in __init__, and threading and queue are imported at the top. Disabling the Run button while a check is running stops a double-click from starting two threads.

4. Keep the logic out of the GUI

The checks themselves live in a plain function with no Tkinter in it. It takes a path and yields (row, problem) pairs:

import csv

REQUIRED = ["isin", "quantity", "price"]

def check_file(path):
    with open(path, newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        missing = [c for c in REQUIRED if c not in (reader.fieldnames or [])]
        if missing:
            yield (0, f"Missing columns: {', '.join(missing)}")
            return
        seen = set()
        for n, row in enumerate(reader, start=2):   # row 1 is the header
            isin = row["isin"].strip()
            if len(isin) != 12:
                yield (n, f"ISIN '{isin}' is not 12 characters")
            if isin in seen:
                yield (n, f"Duplicate ISIN {isin}")
            seen.add(isin)
            try:
                if float(row["price"]) <= 0:
                    yield (n, "Price is zero or negative")
            except ValueError:
                yield (n, f"Price '{row['price']}' is not a number")

Because it knows nothing about windows, you can unit-test it with a small sample file, call it from a scheduled job, or later move it behind a web page without rewriting it. The GUI is a thin layer that collects a path and displays results. The checks here are deliberately simple; a real version would add a proper ISIN check-digit test and rules for your own file layout.

Finishing touches

The entry point is three lines: create tk.Tk(), set a title, create App(root) and call root.mainloop(). From there, a few small additions make the tool feel finished: a scrollbar on the table, an “Export results” button that writes the problems to CSV, and keyboard shortcuts with bind. For packaging, tools like PyInstaller can bundle the script into a single executable so colleagues do not need Python installed, though check your organisation’s policy on running unsigned executables first.

For going deeper, TkDocs is the best modern tutorial I know of, and the official tkinter documentation links to the Tk reference for every widget option.

Further reading

  1. Original article on Medium (external site) · Mrcloudexplorer
  2. tkinter: Python interface to Tcl/Tk (external site) · Python Software Foundation
  3. tkinter.ttk: Tk themed widgets (external site) · Python Software Foundation
  4. Tkinter Dialogs (external site) · Python Software Foundation
  5. TkDocs Tutorial (external site) · Mark Roseman

Drafted with AI assistance and edited by the author.