2

I have an example Python program that sets the text of a tkinter label, and the use of Label.config(text=string) is leaking memory. Other techniques for setting the text don't. Is something wrong here?

I was asked to look at a Python program using tkinter that was leaking memory. I've traced this to its use of Label.config(text=string) to set the text of a label in a watchdog routine that runs frequently. I'm not a Python expert, but there is example code for this on the very useful GeeksforGeeks website at https://www.geeksforgeeks.org/how-to-change-the-tkinter-label-text I assume this code should be reasonably OK, but it seems to demonstrate the leak too.

I have a slightly modified version of that example code with a tracemalloc() call to show the leak. If you run this as shown below, each time you click on the button it resets the text of the label (after the first click it just keeps setting to the same message, but that doesn't matter here). I added the code to print out the memory usage, and it simply goes up and up each time you click and it executes the line:

my_label.config(text = my_text)

Now if you comment out that line and use

my_label["text"] = my_text

it no longer leaks memory. Using a text variable works too.

Obviously, there are potential solutions here, but I'd like to understand why this happens. I'm running this on OS X, but I'm told the same thing works on Windows and on the Raspberry Pi used for the original program, where memory is at more of a premium. It seems connected with garbage collection, because if I import gc and add code like

if (tracemalloc.get_traced_memory()[0] > 2000) : gc.collect()

then it memory leakage gets capped at that 2000 value.

Here's my example program: all I've done is import tracemalloc, add the call to print the memory usage and add the alternative call to set the label text. Both calls set the label text as intended, but one leaks and the other doesn't. Is something wrong here?

# importing everything from tkinter
from tkinter import *
import tracemalloc

# creating the tkinter window
Main_window = Tk()

# variable
my_text = "GeeksforGeeks updated !!!"

# function define for
# updating the my_label
# widget content
def counter():
  
    # use global variable
    global my_text
    
    # configure - one of these leaks, the other doesn't...
    my_label.config(text = my_text)
    #my_label["text"] = my_text
    print (tracemalloc.get_traced_memory()[0])

# create a button widget and attached
# with counter function
my_button = Button(Main_window,
                   text = "Please update",
                   command = counter)

# create a Label widget
my_label = Label(Main_window,
                 text = "geeksforgeeks")

# place the widgets
# in the gui window
my_label.pack()
my_button.pack()

tracemalloc.start()

# Start the GUI
Main_window.mainloop()
9
  • How do you detect the memory leaks? How big are the memory leaks? It's possible that python allocates memory and just doesn't return it to back to the OS or it doesn't immediately garbage collect it after it does out of scope. Also how did you install python? Are you sure you are using the normal python interpreter and not something like PyPy (which has a different garbage collector)? Commented Jun 11 at 14:43
  • On the surface this seems hard to rationalize. my_label["text'] is just syntactic sugar. Eventually, tkinter will call the configure method on the object. Commented Jun 11 at 15:00
  • 4
    If you look tkinter source code you'll see that __setitem__ for a tkinter widget directly calls the configure method. Commented Jun 11 at 15:07
  • 2
    The difference seems to be how the parameters are passed to the configure (or _configure) method. my_label["text"] = my_text (or my_label.configure({"text": my_text})) passes them as a dictionary in cnf, while my_label.configure(text=my_text) passes them in kw instead. This leads to an extra step in _cnfmerge where an extra dict cnf is created. Commented Jun 12 at 11:23
  • 1
    @KeithS is that specific memory leak causing the issues? It might be a bug in the python interpreter but it's more likely that the interpreter delays garbage collection for that specific dict object for a while which should become a real problem. Commented Jun 12 at 17:26

1 Answer 1

0

Prompted by the comments I got, I’ve looked more closely at what happens if my test program is left to run and run. The results are interesting, and while I don’t entirely understand what’s going on, it does suggest that there isn’t actually a runaway memory leak here, so the issue more or less goes away. But in the course of these tests, I did hit what does seem to be a way to trigger a runaway leak by slightly careless coding, and this seems to have been the issue with the original RaspberryPi code, which is now fixed.

In the revised test code below, counter() is triggered by pressing the ‘Start’ button, and then reschedules itself at 10Hz. On just the first time through (with count set to zero), it changes the ‘Start’ button to a ‘Stop’ button that will cause the loop rescheduling to stop. Otherwise, each time through the loop, it updates the text in two labels, one to show the loop count and the other to show the allocated memory. And it keeps running.

By default, it sets the label text using .config(text=string). On my MacOS laptop, the memory usage keeps going up and up and looks as if it’s running away. But then, just after count 1000, the memory hits about 103,000 bytes and then stops going up. (I’m assuming that the .config() call creates a dict with the (single) keyword/value pair that are then used to set the label, that this dict is not deleted and this happens each time through the loop, and so the memory for these dicts keeps going up. And then at some point the Python garbage collection kicks in, and while it doesn’t release the memory, it does stop it increasing. I make no claim to any sort of deep understanding of Python memory management, and it would be interesting to know just what is happening here.) This behaviour seems a little odd, but it isn’t normally going to be a problem.

Change the code line test_config = True to set it False, and the code uses the [“text”] = string form to set the labels. Now the memory starts to increase a little, but at a count of a bit over 30, the memory usage hits around 1200 and then stops increasing. The same sort of behaviour, but scaled down significantly.

At that point, I’m left with behaviour that is slightly curious, but not problematic, and that probably settles the question well enough.

But I originally wrote this fairly quickly, and I didn’t bother to test for the first time through the loop when resetting the button operation. I just reconfigured it each time. Unnecessary, and careless, but surely not importantly so? Apparently not. In the test code, if the line test_stop = False is changed to set to True, the command tied to the button is reconfigured (using [“command”]=routine, but config() has the same effect) each time through the loop, and now there really does appear to be a runaway increase in memory usage.

As it turns out, this was the real problem in the original RaspberryPi program I was looking into. It’s easy to avoid, but it’s also a relatively easy trap to fall into, and it seems to have consequences.

I’ve now had this test program run on Windows, and on a RaspberryPi. The actual numbers vary very slightly, but the behaviour seems the same on all platforms.

from tkinter import *
import tracemalloc
import gc

test_config = True
test_stop = False

Main_window = Tk()

count = 0
resched = True

def Stop():
    global resched
    resched = False

def counter():
  
    global count
    global resched
    
    mem = tracemalloc.get_traced_memory()[0]
    count_str = str(count)
    mem_str = str(mem)
    if (test_config) :
        my_label.config(text = count_str)
        my_mem.config(text = mem_str)
    else :
        my_label["text"] = count_str
        my_mem["text"] = mem_str
        
    if (resched) : Main_window.after(100,counter)
    if (count == 0) :
        my_button.config(text = "Press to stop",command = Stop)
    
    if (test_stop) :
        my_button["command"] = Stop
        #if (count % 100 == 0) : print (sys.getrefcount(Stop))
    count = count + 1

my_button = Button(Main_window,
                   text = "Press to start",
                   command = counter)

my_label = Label(Main_window,text = "Leaks and labels")
my_mem = Label(Main_window,text = "")

my_label.pack()
my_mem.pack()
my_button.pack()

tracemalloc.start()

Main_window.mainloop()

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.