Tk after() Function (tkinter)

The tkinter.Tk.after() function allows you to program the execution of your own function so that it is executed after a certain amount of time. However, it can also be used to tell Tk to execute a function every a given amount of time, which is the most common usage. Let's start by creating a simple window with a label (tk.Label):

import tkinter as tk
root = tk.Tk()
root.title("Tk after() Example")
root.config(width=400, height=300)
label1 = tk.Label(text="Hello world!")
label1.place(x=100, y=70)
root.mainloop()

Suppose now that we want to display in label1 a random number between 1 and 100 every two seconds. The code that initially comes to mind is something like the following:

import random
import time
while True:
    # Get a random number and display it in the label.
    random_number = random.randint(1, 100)
    label1.config(text=f"Random number: {random_number}")
    # Wait 2 seconds before doing it again.
    time.sleep(2)

The problem is that wherever we put this code, it will block Tk's main loop and the UI will get frozen (on this problem, see also Background Tasks With Tk (tkinter)). One possible solution is to move the loop into a child thread, so that the main thread, in which the main window runs, can continue with its tasks normally. This would be fine if the code we move to the new thread doesn't have to interact with the window. But, in this case, we need to update the label1 text every time we generate a new random number. Since Tk runs on a single thread, it is not safe to change any aspect of the user interface from a secondary thread. The correct solution, then, will come from after().

Since after() allows us to tell Tk that it has to execute our function after a certain amount of time has elapsed, we can take a first step by creating a function with the code to generate the random number and display it in the label.

import random
# [...]
def update_label():
    random_number = random.randint(1, 100)
    label1.config(text=f"Random number: {random_number}")

And then program the call to this function before invoking the main loop:

# Execute the update_label() function in two seconds
# (2000 milliseconds).
root.after(2000, update_label)

Thus, the code so far would be:

import tkinter as tk
import random
def update_label():
    random_number = random.randint(1, 100)
    label1.config(text=f"Random number: {random_number}")
root = tk.Tk()
root.title("Tk after() Example")
root.config(width=400, height=300)
label1 = tk.Label(text="Hello world!")
label1.place(x=100, y=70)
root.after(2000, update_label)
root.mainloop()

If we run the program, we will see that the label has the text "Hello world!" for two seconds and then changes to a random number. But now we want it to, again, after two seconds, generate another random number, and then wait another two seconds, and generate another, and so on. That is, update the label with a new random number every two seconds. We can achieve this result very simply if we add a new after() at the end of the function:

def update_label():
    random_number = random.randint(1, 100)
    label1.config(text=f"Random number: {random_number}")
    # Re-schedule this function to run in two seconds.
    root.after(2000, update_label)

Now our label is indeed updated every two seconds:

/images/tk-after-function-tkinter/tkinter-after-random-number.gif

For more complex situations, it is possible to indicate which arguments we want to be passed to our function, and thus avoid the use of global variables. For example, how could we go about adding the numbers instead of replacing one random number with another? Inside the function we should know what the previous random number was, or better said, what is the sum of all the previous random numbers.

# accumulator is zero by default, since the first time the function
# is called no argument is passed.
def update_label(accumulator=0):
    random_number = random.randint(1, 100)
    # Add the above generated random number to the sum of the
    # previous numbers.
    accumulator += random_number
    label1.config(text=f"Sum of random numbers: {accumulator}")
    # Pass the accumulator as an argument to this function, that
    # will be executed again after two seconds.
    root.after(2000, update_label, accumulator)

Arguments can also be used to pass widgets to the function, like label1 or the root itself, if you don't want to use global variable or can't access them (because they are in anothe module, for example.

In conclusion, the structure of the after() function is as follows:

tkinter.Tk.after(milliseconds, function_name, *arguments)

That is, after the first two arguments, all subsequent values will be passed in that same order to the indicated function. If we program a function like this:

root.after(5000, my_function, a, b)

Tk will take care of executing my_function(a, b) within five seconds (5000 milliseconds). If we need Tk to pass to my_function some argument by name, for example, my_function(c=5), we can use the standard functools.partial():

root.after(5000, functools.partial(my_function, c=5))

This can also be combined with positional arguments, such as:

# This will call my_function(a, b, c=5) after five seconds.
root.after(5000, functools.partial(my_function, c=5), a, b)

Finally, it is possible to cancel a scheduled function before it is executed via after_cancel():

task_id = root.after(2000, update_label)
# (Elsewhere in the code.) Cancel the execution of update_label().
root.after_cancel(task_id)