Repository navigation
Memory Leak with Frequent Updates to tk.StringVar in Dynamically Generated Widgets #123193
Description
Bug report
Bug description:
Description
I have encountered a potential memory leak in Tkinter that occurs when dynamically generated widgets are frequently updated. Specifically, the issue arises when tk.StringVar is used to display and update text in labels. When the displayed text is rapidly and frequently changed, the memory consumption steadily grows without being released.
Details:
The problem manifests in applications where widgets are dynamically created and destroyed while being continuously updated with new values. In my specific case, tk.StringVar objects are used to control the text of labels. Each label is dynamically generated in a loop, displayed, and then destroyed to make room for new labels.
Even though the widgets are properly destroyed, the memory usage remains high and continues to increase as the StringVar objects are updated frequently.
System Information:
- Operating System: Windows 10 (64-bit)
- Python Version: 3.10.0 (in a Conda environment)
- Tkinter Version: Standard version shipped with Python 3.10
Minimal Reproduction Example
Here is a minimal Python script that reliably reproduces the issue:
import tkinter as tk
import threading
import time
class MemoryLeakDemo:
def __init__(self, master):
self.master = master
self.master.title("Memory Leak Demo")
# Set a fixed window size
self.master.geometry("400x300") # Width x Height
# Create a scrollable frame
self.canvas = tk.Canvas(self.master)
self.scrollbar = tk.Scrollbar(self.master, orient="vertical", command=self.canvas.yview)
self.scrollable_frame = tk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")
)
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.pack(side="left", fill="both", expand=True)
self.scrollbar.pack(side="right", fill="y")
# Button to start the dynamic updates
self.start_button = tk.Button(self.scrollable_frame, text="Start Dynamic Updates", command=self.start_dynamic_updates)
self.start_button.pack(pady=10)
self.stop_button = tk.Button(self.scrollable_frame, text="Stop Dynamic Updates", command=self.stop_dynamic_updates)
self.stop_button.pack(pady=10)
self.labels = []
self.running = False
self.update_thread = None
def start_dynamic_updates(self):
if not self.running:
self.running = True
self.update_thread = threading.Thread(target=self.dynamic_update)
self.update_thread.start()
def dynamic_update(self):
"""
Simulates dynamically creating and destroying widgets with StringVar updates.
"""
while self.running:
# Clear previous labels (simulating dynamic removal)
for label in self.labels:
label.destroy()
self.labels.clear()
# Create new labels with StringVars
for i in range(10):
text_var = tk.StringVar()
text_var.set(f"Dynamic Label {i}: {time.time()}")
label = tk.Label(self.scrollable_frame, textvariable=text_var)
label.pack()
self.labels.append(label)
# Simulate rapid updates
time.sleep(0.05)
def stop_dynamic_updates(self):
self.running = False
if self.update_thread:
self.update_thread.join()
def on_close(self):
self.stop_dynamic_updates()
self.master.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = MemoryLeakDemo(root)
root.protocol("WM_DELETE_WINDOW", app.on_close)
root.mainloop()Steps to Reproduce:
- Run the script above.
- Click "Start Dynamic Updates."
- Monitor the application's memory usage over time. You should observe a steady increase in memory consumption, even though the dynamically generated widgets are being destroyed and removed.
Expected Behavior:
The memory should remain stable when widgets are dynamically removed and recreated.
Actual Behavior:
The memory usage continuously increases, indicating a potential memory leak.
Possible Cause:
It appears that tk.StringVar objects, when frequently updated and dynamically assigned, are not being properly released when the widgets are destroyed.
Additional Notes:
The issue is more pronounced when updates happen in very short intervals (e.g., every 50 milliseconds) while widgets are being dynamically created and destroyed.
CPython versions tested on:
3.10
Operating systems tested on:
Windows
Activity
ZeroIntensity commentedon Aug 21, 2024
I haven't had a chance to try and reproduce this, but at a glance, could this be related to a reference cycle? Add a
gc.collectbetween iterations to trigger a collection cycle, and see if it still occurs.terryjreedy commentedon Aug 21, 2024
To do anything, we would have to know if the leak is in python (_tkinter) or tcl/tk (or somehow both?). If tcl/tk only, report on their tracker.
ZeroIntensity commentedon Aug 22, 2024
I can try to run this with a memory profiler sometime later today, but I do see a potential candidate for the problem.
cpython/Modules/_tkinter.c
Line 1755 in 3d7b1a5
AFAICS,
evis not deallocated (unlessTcl_ThreadQueueEventdoes it, which would be odd.)picnixz commentedon Aug 22, 2024
I think this is the case (see https://linux.die.net/man/3/tcl_threadqueueevent) (emphasis mine):
In addition, the other occurrences of using
Tkapp_ThreadSendseems to suggest that theevpointer is no more owned after the call.ZeroIntensity commentedon Aug 22, 2024
@picnixz were you able to reproduce this? I haven't been able to. Memray is showing some leaks on false positives (e.g. the
mainloopcall), butStringVarisn't even showing up on the flamegraph. Likewise,htopdoesn't show any memory increase over time, it stays continuous as expected.@OberstMoeb how long were you running it for? I only ran it for a minute or two (generally, that's more than enough to see a leak, but maybe not in this case.)
OberstMoeb commentedon Aug 22, 2024
The leak occurs for me after 10 second. May it is a problem, which is only coming up on windows.
ZeroIntensity commentedon Aug 22, 2024
A speculative guess could be that Windows uses lazy memory deallocation. It knows that the memory is freed, but since you're going to request some memory again later, it doesn't bother actually letting other processes use it, and just keeps it available in the current process, so the memory remains "unfreed" until the end the process ends.
tkinterisn't using Python's special allocator (as shown above), so objects aren't reused, and hence it looks like the memory is increasing over time. I'm not a Windows expert though; please correct me if I'm wrong!picnixz commentedon Aug 23, 2024
No because I was too lazy and too busy for that. I was chiming in for the tcl details.
gh-123193: Document tkinter Variable lifetime (GH-152625)
[3.13] gh-123193: Document tkinter Variable lifetime (GH-152625) (GH-…
[3.14] gh-123193: Document tkinter Variable lifetime (GH-152625) (GH-…
[3.15] gh-123193: Document tkinter Variable lifetime (GH-152625) (GH-…
serhiy-storchaka commentedon Jul 5, 2026
This is expected behavior rather than a leak in tkinter itself: each
Variable(StringVar, etc.) is backed by a Tcl variable that lives until the PythonVariableobject is deleted. Creating a freshVariablefor every update accumulates them; reuse a singleVariableand update its value (or drop references to the ones you no longer need) instead.The
Variablelifetime is now documented in #152625, so I'm closing this as documented. Thanks for the report!