Skip to content

Memory Leak with Frequent Updates to tk.StringVar in Dynamically Generated Widgets #123193

Closed

Description

@OberstMoeb

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:

  1. Run the script above.
  2. Click "Start Dynamic Updates."
  3. 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

Linked PRs

Activity

  1. ZeroIntensity commented on Aug 21, 2024

    @ZeroIntensity
    Member

    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.collect between iterations to trigger a collection cycle, and see if it still occurs.

  2. terryjreedy commented on Aug 21, 2024

    @terryjreedy
    Member

    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.

  3. ZeroIntensity commented on Aug 22, 2024

    @ZeroIntensity
    Member

    I can try to run this with a memory profiler sometime later today, but I do see a potential candidate for the problem.

    ev = (VarEvent*)attemptckalloc(sizeof(VarEvent));

    AFAICS, ev is not deallocated (unless Tcl_ThreadQueueEvent does it, which would be odd.)

  4. picnixz commented on Aug 22, 2024

    @picnixz
    Member

    AFAICS, ev is not deallocated (unless Tcl_ThreadQueueEvent does it, which would be odd.)

    I think this is the case (see https://linux.die.net/man/3/tcl_threadqueueevent) (emphasis mine):

    When proc returns 1, Tcl_ServiceEvent will remove the event from the event queue and free its storage. Note that the storage for an event must be allocated by the event source (using Tcl_Alloc or the Tcl macro ckalloc) before calling Tcl_QueueEvent, but it will be freed by Tcl_ServiceEvent, not by the event source.

    Threaded applications work in a similar manner, except that there is a separate event queue for each thread containing a Tcl interpreter

    In addition, the other occurrences of using Tkapp_ThreadSend seems to suggest that the ev pointer is no more owned after the call.

  5. ZeroIntensity commented on Aug 22, 2024

    @ZeroIntensity
    Member

    @picnixz were you able to reproduce this? I haven't been able to. Memray is showing some leaks on false positives (e.g. the mainloop call), but StringVar isn't even showing up on the flamegraph. Likewise, htop doesn'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.)

  6. OberstMoeb commented on Aug 22, 2024

    @OberstMoeb
    Author

    The leak occurs for me after 10 second. May it is a problem, which is only coming up on windows.

  7. ZeroIntensity commented on Aug 22, 2024

    @ZeroIntensity
    Member

    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.

    tkinter isn'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!

  8. picnixz commented on Aug 23, 2024

    @picnixz
    Member

    were you able to reproduce this?

    No because I was too lazy and too busy for that. I was chiming in for the tcl details.

  9. added 4 commits that reference this issue on Jul 5, 2026
  10. serhiy-storchaka commented on Jul 5, 2026

    @serhiy-storchaka
    Member

    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 Python Variable object is deleted. Creating a fresh Variable for every update accumulates them; reuse a single Variable and update its value (or drop references to the ones you no longer need) instead.

    The Variable lifetime is now documented in #152625, so I'm closing this as documented. Thanks for the report!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions