I am working on a camera applicaiton where I need to get images from camera buffer and display it in a TK label live: def display_thread_run(self): while self.is_acquiring: image_data = self.camera.get_next_image() # --- some image conversion methods omitted --- pil_image = Image.fromarray(image_data) photo = ImageTk.PhotoImage(image=pil_image) self.image_frame.after(0, self._create_photo_and_update_gui, photo) time.sleep(0.01) When the application exits, I need to close the camera stream, join the thread and at the very end, release camera related resources: def stop_and_cleanup(self): camera.stop_acquisition() self.is_acquiring = False if self.display_thread and self.display_thread.is_alive(): self.display_thread.join() camera.release() logger.info("Display resources released") However, after I click the close window button (cross button), photo = ImageTk.PhotoImage(image=pil_image) causes deadlock and the thread won't join. Setting thread.daemon=True and removing display_thread.join() won't work because camera.release() must be called after the thread is properly cleaned. I'm using: Python 3.10.11 pillow 11.2.1 A minimum code example: import tkinter as tk from PIL import Image, ImageTk import threading import time IMG_PATH = 'test_img.jpg' class App: def __init__(self, root): self.root = root self.label = tk.Label(root) self.label.pack() self.running = True self.img = Image.open(IMG_PATH) self.display_thread= threading.Thread(target=self.update_image_loop, daemon=True) self.display_thread.start() self.root.protocol("WM_DELETE_WINDOW", self.on_close) def update_image_loop(self): while self.running: tk_img = ImageTk.PhotoImage(image=self.img) self.label.after(0, self.set_image, tk_img) time.sleep(0.001) def set_image(self, tk_img): self.label.img = tk_img # keep reference self.label.config(image=tk_img) def on_close(self): print("on_close") self.running = False self.display_thread.join() self.root.destroy() if __name__ == '__main__': root = tk.Tk() app = App(root) root.mainloop() Continue reading...