2025, Dec 18 13:00

How to build real-time Tkinter labels without TclError on close: StringVar updates and a clean shutdown loop

Learn real-time UI updates in Tkinter without crashes: fix TclError on close, stop PY_VAR labels, and use StringVar with a clean, responsive manual loop.

Real-time UI updates in desktop apps look trivial until you try to close the window while a background loop is still hammering the toolkit. A common case is a quick Tkinter panel that shows a fast-changing value, for example time.time() while prototyping a photon counter. The expectation is simple: show the value as fast as possible and let the rest of the program continue once the panel is closed. The initial approach often works visually, but closing the window triggers a TclError, or the label stops updating and prints PY_VAR instead of the number. Let’s unpack why this happens and how to make the update loop behave correctly.

Minimal example that triggers the error

The following script continuously updates a label inside an explicit while loop. It looks fine until you click the close button, after which TclError appears because the object it tries to update no longer exists.

import numpy as np
import matplotlib.pyplot as plt
import tkinter as tk
import time

class Dashboard:
    def __init__(self, host: tk.Tk, label_text: str):
        self.host = host
        self.current_val = time.time()
        host.title('A simple window')
        self.readout = tk.Label(host, text=self.current_val)
        self.readout.pack()

    def refresh_label(self, payload):
        self.current_val = time.time()
        self.readout.config(text=self.current_val)

root = tk.Tk()
ui = Dashboard(root, "HOLA")

while True:
    ui.refresh_label(time.time())
    ui.host.update_idletasks()
    ui.host.update()

root.mainloop()

Closing the window here raises TclError: invalid command name ".!label". The loop still calls .config on a label that Tk has already destroyed. The UI thread keeps running and tries to talk to a widget that no longer exists.

Why PY_VAR shows up and the panel stops updating

Another variant replaces text with a StringVar and attempts to self-schedule updates using after. However, the code sets the StringVar to itself, not to a new value, and the label displays the variable’s internal name (PY_VAR...). There’s also no external update loop feeding a fresh time value anymore, so the readout doesn’t change to current time.

import tkinter as tk
import time

class Panel:
    def __init__(self, parent: tk.Tk, seed_value):
        self.parent = parent
        self.data = tk.StringVar()
        self.data.set(seed_value)
        parent.title("A simple window")
        self.view = tk.Label(parent, textvariable=self.data)
        self.view.pack()
        self.view.after(1, self.tick)

    def tick(self):
        self.data.set(self.data)
        self.view.after(1, self.tick)

root = tk.Tk()
screen = Panel(root, time.time())
root.mainloop()

The root cause is straightforward. Tying a label to a StringVar is correct, but assigning the variable to itself turns the value into the object’s default string representation. Without a source that updates the number, nothing changes beyond that.

A working pattern that keeps the UI responsive and stops cleanly

A reliable approach is to keep a small state flag that controls a manual loop which mirrors mainloop, and to wire the Quit action and the window close protocol to the same shutdown method. Updating a StringVar with a new value then automatically refreshes the label. Once the user quits, you destroy the window, flip the flag, and let the rest of the program continue.

import tkinter as tk
import time

class MonitorUI:
    def __init__(self, app_root):
        self.active = True
        self.window = app_root
        self.counter_var = tk.StringVar()
        self.counter_var.set("Starting...")
        self.window.title("A simple window")
        tk.Button(self.window, text="Quit", command=self.shutdown).pack()
        self.output = tk.Label(self.window, textvariable=self.counter_var)
        self.output.pack()
        self.window.protocol("WM_DELETE_WINDOW", self.shutdown)

    def set_value(self, val):
        self.counter_var.set(val)

    def shutdown(self):
        self.active = False
        self.window.destroy()

app = tk.Tk()
ui = MonitorUI(app)
while ui.active:
    ui.set_value(time.time())
    ui.window.update_idletasks()
    ui.window.update()

This pattern ensures that the label updates as quickly as your loop drives it, while allowing the application to exit cleanly. The closing action and the Quit button both end up in the same shutdown path, so you won’t chase different exit behaviors. Using StringVar ensures the label refresh happens automatically after each set, and there is no need to call mainloop at the bottom because the explicit loop, combined with update_idletasks and update, already drives the Tk event processing.

Why this matters

When you need a panel that can be opened, updated rapidly, and closed without halting other work, you must coordinate two concerns: do not call into destroyed widgets, and update the readout using a mechanism that the toolkit understands. The approach above answers both. It also accounts for the practical reality that time.sleep isn’t a good fit for driving UI refreshes in this context, and that using after is constrained by millisecond granularity. If you need updates as fast as possible, pushing new values within the manual loop keeps the UI moving while remaining under your control.

Summary and guidance

A label tied to a StringVar will reflect whatever you set on that variable, so set it to the actual value rather than the variable object itself. If you drive the UI with an explicit loop, make that loop conditional on a state flag and connect both the close action and a Quit button to a shutdown method that flips the flag and destroys the window. That avoids TclError after the window closes and lets your program continue its work. For extremely fast updates, remember that after won’t schedule less than a millisecond, so feeding values through the manual loop is an effective way to refresh the display at the pace you require.