Question: I am making a timer using tkinter in python. The widget simply has a single button. This button doubles as the element displaying the time remaining. The timer has a thread that simply updates what time is shown on the button. The thread simply uses a while loop that should stop when an event is set. When the window is closed, I use protocol to call a function that sets this event then attempts to join the thread. This works most of the time. However, if I close the program just as a certain call is being made, this fails and the thread continues after the window has been closed. I'm aware of the other similar threads about closing threads when closing a tkinter window. But these answers are old, and I would like to avoid using thread.stop() if possible. I tried reducing this as much as possible while still showing my intentions for the program. import tkinter as tk from tkinter import TclError, ttk from datetime import timedelta import time import threading from threading import Event def strfdelta(tdelta): # Includes microseconds hours, rem = divmod(tdelta.seconds, 3600) minutes, seconds = divmod(rem, 60) return str(hours).rjust(2, '0') + ":" + str(minutes).rjust(2, '0') + \ ":" + str(seconds).rjust(2, '0') + ":" + str(tdelta.microseconds).rjust(6, '0')[0:2] class App(tk.Tk): def __init__(self): super().__init__() self.is_running = False is_closing = Event() self.start_time = timedelta(seconds=4, microseconds=10, minutes=0, hours=0) self.current_start_time = self.start_time self.time_of_last_pause = time.time() self.time_of_last_unpause = None # region guisetup self.time_display = None self.geometry("320x110") self.title('Replace') self.resizable(False, False) box1 = self.create_top_box(self) box1.place(x=0, y=0) # endregion guisetup self.timer_thread = threading.Thread(target=self.timer_run_loop, args=(is_closing, )) self.timer_thread.start() def on_close(): # This occasionally fails when we try to close. is_closing.set() # This used to be a boolean property self.is_closing. Making it an event didn't help. print("on_close()") try: self.timer_thread.join(timeout=2) finally: if self.timer_thread.is_alive(): self.timer_thread.join(timeout=2) if self.timer_thread.is_alive(): print("timer thread is still alive again..") else: print("timer thread is finally finished") else: print("timer thread finished2") self.destroy() # https://stackoverflow.com/questions/111155/how-do-i-handle-the-window-close-event-in-tkinter self.protocol("WM_DELETE_WINDOW", on_close) def create_top_box(self, container): box = tk.Frame(container, height=110, width=320) box_m = tk.Frame(box, bg="blue", width=320, height=110) box_m.place(x=0, y=0) self.time_display = tk.Button(box_m, text=strfdelta(self.start_time), command=self.toggle_timer_state) self.time_display.place(x=25, y=20) return box def update_shown_time(self, time_to_show: timedelta = None): print("timer_run_loop must finish. flag 0015") # If the window closes at this point, everything freezes self.time_display.configure(text=strfdelta(time_to_show)) print("timer_run_loop must finish. flag 016") def toggle_timer_state(self): # update time_of_last_unpause if it has never been set if not self.is_running and self.time_of_last_unpause is None: self.time_of_last_unpause = time.time() if self.is_running: self.pause_timer() else: self.start_timer_running() def pause_timer(self): pass # Uses self.time_of_last_unpause, Alters self.is_running, self.time_of_last_pause, self.current_start_time def timer_run_loop(self, event): while not event.is_set(): if not self.is_running: print("timer_run_loop must finish. flag 008") self.update_shown_time(self.current_start_time) print("timer_run_loop must finish. flag 018") print("timer_run_loop() ending") def start_timer_running(self): pass # Uses self.current_start_time; Alters self.is_running, self.time_of_last_unpause if __name__ == "__main__": app = App() app.mainloop()  You don't even have to press the button for this bug to manifest, but it does take trail and error. I just run it and hit alt f4 until it happens. If you run this and encounter the problem, you will see that "timer_run_loop must finish. flag 0015" is the last thing printed before we check if the thread has ended. That means, self.time_display.configure(text=strfdelta(time_to_show)) hasn't finished yet. I think closing the tkinter window while a thread is using this tkinter button inside of it is somehow causing a problem. There seems to be very little solid documentation about the configure method in tkinter. Python's official documention of tkinter mentions the function only in passing. It's just used as a read-only dictionary. A tkinter style class gets a little bit of detail about it's configure method, but this is unhelpful. The tkdocs lists configure aka config as one of the methods available for all widgets. This tutorial article seems to be the only place that shows the function actually being used. But it doesn't mention any possible problems or exceptions the method could encounter. Is there some resource sharing pattern I'm not using? Or is there a better way to end this thread?

Answer: Ok, so, first I would like to introduce the .after method, which can be used in conjunction with your widget, not requiring the use of threads Notice that the update_time function is called once and calls itself again, making the loop not interfere with tkinter's mainloop in general. This will close along with the program without any problems. import datetime from tkinter import * start_time = datetime.datetime.now() def update_timer(): current_time = datetime.datetime.now() timer_label.config(text=f'{current_time - start_time}') root.after(1000, update_timer) root = Tk() timer_label = Label(text='0') timer_label.pack() update_timer() root.mainloop()  Now some explanation about Non-Daemon Threads... When you create a non-daemon thread, it will run until it finishes executing, that is, it will remain open even if the parent is closed, until the process ends. # import module from threading import * import time # creating a function def thread_1(): for i in range(5): print('this is non-daemon thread') time.sleep(2) # creating a thread T T = Thread(target=thread_1) # starting of thread T T.start() # main thread stop execution till 5 sec. time.sleep(5) print('main Thread execution')  Output this is non-daemon thread this is non-daemon thread this is non-daemon thread main Thread execution this is non-daemon thread this is non-daemon thread  Now see the same example using a daemon thread, this thread will respect the execution nature of the main thread, that is, if it stops, the 'subthread' stops too. # import modules import time from threading import * # creating a function def thread_1(): for i in range(5): print('this is thread T') time.sleep(3) # creating a thread T = Thread(target=thread_1, daemon=True) # starting of Thread T T.start() time.sleep(5) print('this is Main Thread')  this is thread T this is thread T this is Main Thread  That said, my main solution would be to use .after, if even this solution fails and you need to use threads, use threads daemon=True, this will correctly close the threads after you close your app with tkinter You can see more on Python Daemon Threads