pythonmemorytkinterpsutil

Why doesn't tkinter release memory when an instance is destroyed?


I wanted a quick and dirty way to get some file names without typing in my shell, so I have this following piece of code:

from tkinter.filedialog import askopenfile

file = askopenfile()

Now this all works fine, but it does create a superfluous tkinter GUI that needs to be closed. I know I can do this to suppress it:

import tkinter as tk
tk.Tk().withdraw()    

But it doesn't mean it's not loaded on the back. It just means now there's a Tk() object that I can't close/destroy.


So this brought me to my real question.

It seems each time I create a Tk(), regardless if I del or destroy() it, the memory isn't freed up. See below:

import tkinter as tk
import os, psutil
process = psutil.Process(os.getpid())
def mem(): print(f'{process.memory_info().rss:,}')

# initial memory usage
mem()

# 21,475,328
for i in range(20):
    root.append(tk.Tk())
    root[-1].destroy()
    mem()

# 24,952,832
# 26,251,264
# ...
# 47,591,424
# 48,865,280

# try deleting the root instead

del root
mem()

# 50,819,072

As seen, python doesn't free up the usage even after every instance of Tk() is destroyed and roots deleted. This however isn't the case for other objects:

class Foo():
    def __init__(self):
        # create a list that takes up approximately the same size as a Tk() on average
        self.lst = list(range(11500))    

for i in range(20):
    root.append(Foo())
    del root[-1]
    mem()

# 52,162,560
# 52,162,560
# ...
# 52,162,560

So my question is, why is it different between Tk() and my Foo(), and why doesn't destroying/deleting the Tk() created free up the memory taken up?

Is there something obvious I've missed? Is my test inadequate to confirm my suspicion? I've searched here and Google but found little answers.

Edit: Below are a few other methods I've tried (and failed) with the recommendations in the comments:

# Force garbage collection
import gc
gc.collect()

# quit() method
root.quit()

# delete the entire tkinter reference
del tk

Solution

  • When you create an instance of Tk, you are creating more than just a widget. You are creating an object that has several attributes (an embedded tcl interpreter, a list of widgets, etc). When you do root.destroy(), you're only destroying some of the data owned by that object. The object itself still exists and takes up memory. Since you keep a reference to that object in a list, that object never gets garbage-collected so the memory hangs around.

    When you create a root window with root = tk.Tk(), you get back an object (root). If you look at the attributes of that object with vars, you see the following:

    >>> root = tk.Tk()
    >>> vars(root)
    {'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': ['tkerror', 'exit', '4463962184destroy'], 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}
    

    When you call root.destroy(), you are only destroying the widget itself (essentially, the elements in the _tclCommands list). The other parts of the object remain intact.

    >>> root.destroy()
    >>> vars(root)
    {'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': None, 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}
    

    Notice how _tclCommands has been set to None, but the rest of the attributes are still taking up memory. One of those, tk takes up a fair amount of memory that never gets reclaimed.

    To completely remove the object, you need to delete it. In your case you need to remove the item from the list so that there are no longer any references to the object. You can then wait for the garbage collector to work it's magic, or you can explicitly call the garbage collector.

    This may not reclaim 100% of the memory, but it should get you pretty close.


    All that being said, tkinter wasn't designed to be used this way. The underlying expectation is that you create a single instance of Tk at the start of your program, and keep that single instance alive until your program exits.

    In your case I recommend you create the root window once at the start of the program, and hide it. You can then call askopenfile() as often as you like throughout your program. If you want something more general-purpose, create a function that creates the root window the first time it is called and caches the window so that it only has to create it once.