Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 16 – Practical Python: text, CLIs, web, automation and security

Desktop GUIs with tkinter: a first app

Build a small desktop window with tkinter and ttk widgets, lay it out with grid, answer clicks with callbacks, and keep the logic in functions you can test.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A window reads the inputs, calls the logic and shows the answer · grid lays a window out in rows and columns · mainloop() runs one callback at a time · Check yourself. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Create a window with ttk widgets and arrange them with grid
  • Respond to clicks and keys with callbacks and tkinter variables
  • Explain how mainloop() runs callbacks one at a time
  • Prevent a frozen window during slow work with after() or a worker thread and a queue
  • Test a GUI's logic without a window, and its wiring with a window that is never shown

Before you start

What you’ll learn · Before you start

What you will learn

  • Create a window with ttk widgets and arrange them with grid
  • Respond to clicks and keys with callbacks and tkinter variables
  • Explain how mainloop() runs callbacks one at a time
  • Prevent a frozen window during slow work with after() or a worker thread and a queue
  • Test a GUI's logic without a window, and its wiring with a window that is never shown

Before you start

On this page

Splitting a restaurant bill of 1,499 rupees four ways with a 10 % tip takes three inputs, a button and one line of answer: 413 rupees each. tkinter, the GUI toolkit that comes with Python, builds such a window on top of Tcl/Tk, Tk 9.0.4 in the CPython 3.14.8 build these examples ran on. A tkinter program creates a window, adds widgets such as labels, entries and buttons, places them with a geometry manager, connects each button or key to a function, a callback: A function handed to other code, which calls it at the right moment, such as the function re.sub() calls for every match or the factory a defaultdict calls for a missing key. More in “Lambdas and higher-order functions”, and then calls mainloop(), which waits for events and runs the matching callback.

The themed widgets of tkinter.ttk look like the rest of the operating system, so new code uses them. Two habits make tkinter programs easy to change: keep the calculations in plain functions that know nothing about windows, and keep every callback short, because the window freezes while one runs. The browser’s Python has no tkinter, so the examples ran on a computer, window hidden.

A window reads the inputs, calls the logic and shows the answer

The program splits a restaurant bill: the total, the number of people and a tip go in, each person’s share comes out, rounded up to a whole rupee. The calculation lives in its own module; the window only reads the inputs, calls it and shows the answer:

A bill-splitting window, driven from code with its window hidden

Python · bill_app.py

import sys
import tkinter as tk
from tkinter import ttk

from bill_logic import share


class BillApp(ttk.Frame):
    """The window: entries for the total, the people and the tip, a button, and a line for the result."""

    def __init__(self, parent):
        super().__init__(parent, padding=12)
        self.total = tk.StringVar()
        self.people = tk.IntVar(value=2)
        self.tip = tk.StringVar(value="10")
        self.result = tk.StringVar(value="Enter the bill total")

        ttk.Label(self, text="Bill total (₹)").grid(row=0, column=0, sticky="w", padx=(0, 8))
        ttk.Entry(self, textvariable=self.total, width=12).grid(row=0, column=1, sticky="ew")
        ttk.Label(self, text="People").grid(row=1, column=0, sticky="w", padx=(0, 8))
        ttk.Spinbox(self, from_=1, to=50, textvariable=self.people, width=5).grid(row=1, column=1, sticky="w")
        ttk.Label(self, text="Tip %").grid(row=2, column=0, sticky="w", padx=(0, 8))
        ttk.Combobox(self, values=["0", "5", "10", "15"], textvariable=self.tip, width=5, state="readonly").grid(row=2, column=1, sticky="w")
        self.button = ttk.Button(self, text="Split", command=self.split)
        self.button.grid(row=3, column=0, columnspan=2, pady=(8, 0))
        ttk.Label(self, textvariable=self.result).grid(row=4, column=0, columnspan=2, pady=(8, 0))
        self.columnconfigure(1, weight=1)                  # the entry column takes any extra width
        parent.event_add("<<Split>>", "<Return>", "<KP_Enter>")  # one virtual event for both Enter keys
        parent.bind("<<Split>>", lambda event: self.split())

    def split(self):
        """The callback: read the inputs, call the logic, show the answer or what is wrong."""
        try:
            each = share(self.total.get(), self.people.get(), int(self.tip.get()))
        except (ValueError, tk.TclError) as e:                 # TclError: the People box holds no number
            self.result.set(f"Check the input: {e}")
        else:
            self.result.set(f"Each person pays ₹{each}")


def self_test(root, app):
    """Drive the window from code, the way a person would, and print what it shows."""
    for widget in sorted(app.grid_slaves(), key=lambda w: (int(w.grid_info()["row"]), int(w.grid_info()["column"]))):
        info = widget.grid_info()
        text = widget.cget("text") if widget.winfo_class() in ("TLabel", "TButton") else ""
        print(f"row {info['row']} column {info['column']}: {widget.winfo_class():9} {text}".rstrip())
    for total, people in [("1499", 4), ("2000", 3), ("12a", 2)]:
        app.total.set(total)
        app.people.set(people)
        app.button.invoke()                                     # the same call a click makes
        print(f"total {total!r}, {people} people, tip {app.tip.get()} % -> {app.result.get()}")
    root.destroy()


def main():
    root = tk.Tk()
    root.title("Split the bill")
    app = BillApp(root)
    app.grid(sticky="nsew")
    if "--self-test" in sys.argv:
        root.withdraw()                                         # no window on screen
        self_test(root, app)
    else:
        root.mainloop()                                         # wait for events until the window closes


if __name__ == "__main__":
    main()

Output

row 0 column 0: TLabel    Bill total (₹)
row 0 column 1: TEntry
row 1 column 0: TLabel    People
row 1 column 1: TSpinbox
row 2 column 0: TLabel    Tip %
row 2 column 1: TCombobox
row 3 column 0: TButton   Split
row 4 column 0: TLabel    Enter the bill total
total '1499', 4 people, tip 10 % -> Each person pays ₹413
total '2000', 3 people, tip 10 % -> Each person pays ₹734
total '12a', 2 people, tip 10 % -> Check the input: '12a' is not an amount

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bill_app.py --self-test

bill_logic.py

from decimal import ROUND_CEILING, Decimal, InvalidOperation


def share(total, people, tip_percent=0):
    """Each person's share in rupees, rounded up to a whole rupee. Raises ValueError for input it cannot use."""
    try:
        amount = Decimal(total)
    except InvalidOperation:
        raise ValueError(f"{total!r} is not an amount") from None
    if not amount.is_finite() or amount < 0:
        raise ValueError("the total must be 0 or more")
    if people < 1:
        raise ValueError("at least one person pays")
    with_tip = amount * (100 + tip_percent) / 100
    return (with_tip / people).to_integral_value(rounding=ROUND_CEILING)

Step through the first self-test, one click of Split:

Interactive · message by message One click of Split, from the inputs to the answer Step through the first self-test of bill_app.py. The window only reads and shows; the arithmetic happens in share().
One click of Split, from the inputs to the answer, step 6 of 6. Messages so far: the person to the BillApp window: 1499, 4 people, 10 %; the person to the BillApp window: click Split; the BillApp window to share() in bill_logic: share('1499', 4, 10); share() in bill_logic to the BillApp window: returns 413; share() in bill_logic to the BillApp window: ValueError: '12a'.the persontypes and clicksthe BillApp windowCheck the input: …share() in bill_logicDecimal, rounded up1499, 4 people, 10 %click Splitshare('1499', 4, 10)returns 413ValueError: '12a'

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The inputs. The person types 1499 as the total, sets 4 people and picks a tip of 10 %. Each widget writes what it shows into its tkinter variable.
  2. The click. A click on Split makes Tk call the callback named in command=, self.split(). The self-test calls invoke(), which does exactly what a click does.
  3. Read and hand over. split() reads the three variables with get() and calls share('1499', 4, 10). It does no arithmetic itself.
  4. The answer. share() adds the tip and rounds each person's share up to a whole rupee, so it returns 413.
  5. Shown in the window. split() sets the result variable, and the label tied to it shows Each person pays ₹413 as soon as the callback returns.
  6. Bad input. With 12a as the total, share() raises ValueError. split() catches it and shows Check the input in the window, instead of leaving a traceback on the terminal and a window that does nothing.

  • BillApp is a ttk.Frame holding the widgets. Each input is tied to a tkinter variable: StringVar or IntVar objects whose get() and set() read and change what the widget shows.
  • ttk.Button(..., command=self.split) names the callback, and invoke() calls it exactly as a click does, which is how --self-test used the window without anyone clicking.
  • split() catches ValueError from the logic and TclError from an IntVar that holds no number: a callback that raises leaves a traceback on the terminal and a window that silently does nothing.

Run it without --self-test and root.mainloop() keeps the window open until you close it; the Return key and the keypad’s Enter key split too. To try it, save both files in one folder and run python bill_app.py. The installers from python.org include tkinter on Windows and macOS; on Debian and Ubuntu it is a separate system package, python3-tk.

Checkpoint

Check: A window that reads, calls and shows

  1. Question 1 of 2 What does command=self.split in ttk.Button(self, text="Split", command=self.split) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It names the method that the event loop calls each time the button is clicked

    The method itself is passed, so the event loop can call it on every click, and invoke() calls it the same way from code, which is how the self-test drives the window.

  2. Question 2 of 2 A callback raises ValueError, and nothing in it catches the exception. What does the person at the window see?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing changes in the window; the traceback goes to the terminal, and the window carries on as if nothing happened

    An uncaught exception in a callback leaves a traceback on the terminal and a window that silently does nothing. Catch the errors you expect and show them in the window, as split() does.

grid lays a window out in rows and columns

A widget appears only once a geometry manager has placed it. Tk has three, and every widget can use any of them:

Tk's three geometry managers
Criterion gridpackplace
How it places widgets in rows and columns of a tableone after another against a side: top, bottom, left or rightat coordinates you give, absolute or relative
Resizing columns and rows grow by their weightfill and expand optionsonly what you compute
Typical use forms, dialogs, most windowsa toolbar above a work areaan overlay at an exact spot
When to choose Most windowsSimple stacksRarely

The bill window uses grid: labels in column 0, inputs in column 1, the button and the result spanning both with columnspan=2, and columnconfigure(1, weight=1) so that only the input column grows when the window does. sticky="ew" stretches the entry across its cell from west to east, and padx and pady add space around a widget. The self-test printed the same grid, row by row:

Grid of the bill window: labels in column 0, inputs in column 1, then a Split button and a result line across both columns.Split the billrow 0, column 0Label: Bill total (₹)row 0, column 1Entry, sticky ewrow 1, column 0Label: Peoplerow 1, column 1Spinbox 1 to 50row 2, column 0Label: Tip %row 2, column 1Combobox 0, 5, 10, 15row 3, columns 0 and 1Button: Split(columnspan=2)row 4, columns 0 and 1Label: the result(columnspan=2)

The bill window as grid() lays it out

Text description of the diagram

The window is a grid of five rows and two columns. 1. Row 0: the label "Bill total (₹)" in column 0 and an entry for the amount in column 1, stretched to the column's width with sticky="ew". 2. Row 1: the label "People" and a spinbox that counts from 1 to 50. 3. Row 2: the label "Tip %" and a read-only combobox with 0, 5, 10 and 15. 4. Row 3: the Split button, across both columns with columnspan=2. 5. Row 4: the result line, also across both columns. Column 1 has weight 1, so when the window is made wider, only the column of inputs grows.

The tkinter documentation warns against using pack() and grid() for widgets in the same container, because the two can resize it against each other until the program hangs; give each its own Frame.

Checkpoint

Check: Placing widgets with grid

  1. Question 1 of 2 A program creates ttk.Label(root, text="Total") and nothing appears in the window. What is missing?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A geometry manager call, such as .grid(row=0, column=0), which places the widget

    Every widget needs a geometry manager to appear. Forgetting the grid(), pack() or place() call is a common first mistake.

  2. Question 2 of 2 The person makes the bill window wider. Which part grows, and why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only column 1, the inputs, because columnconfigure(1, weight=1) gives it all the extra width

    grid hands extra space to the columns and rows that have a weight, and sticky decides whether a widget stretches to fill its cell.

mainloop() runs one callback at a time

mainloop() is a loop that waits for the next event and runs its callback. Timers are events too, scheduled with after():

Timers, an idle callback and a button click, in the order the loop ran them Python · event_order.py
import tkinter as tk

root = tk.Tk()
root.withdraw()
order = []

root.after(200, lambda: order.append("after(200): a timer, 200 ms later"))
root.after(0, lambda: order.append("after(0): a timer that is due at once"))
root.after_idle(lambda: order.append("after_idle(): when nothing else is waiting"))
button = tk.Button(root, text="Pay", command=lambda: order.append("the button's command"))
root.after(100, button.invoke)                     # a click, 100 ms after the loop starts
root.after(300, root.quit)                         # end mainloop() after 300 ms
order.append("before mainloop(): nothing above has run yet")

root.mainloop()                                    # runs the callbacks, one at a time, until quit()
order.append("after mainloop() returned")
root.destroy()
print("\n".join(f"{i}. {step}" for i, step in enumerate(order, 1)))

Output

1. before mainloop(): nothing above has run yet
2. after(0): a timer that is due at once
3. after_idle(): when nothing else is waiting
4. the button's command
5. after(200): a timer, 200 ms later
6. after mainloop() returned

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 event_order.py

Step through the run and watch when each callback runs:

Interactive · message by message Five callbacks and the loop that runs them Step through event_order.py. Watch when each registered callback runs, and that only one runs at a time.
Five callbacks and the loop that runs them, step 8 of 8. Messages so far: your code to Tk's waiting list: five callbacks registered; your code to mainloop(): mainloop(); Tk's waiting list to mainloop(): after(0) is due; Tk's waiting list to mainloop(): idle; Tk's waiting list to mainloop(): after(100): invoke(); Tk's waiting list to mainloop(): after(200) is due; mainloop() to your code: mainloop() returns.your code6. after mainloop()mainloop()5. after(200)Tk's waiting list0, 100, 200, 300 ms and idlefive callbacks registeredmainloop()after(0) is dueidleafter(100): invoke()after(200) is duemainloop() returns

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Registered, not run. The program registers timers of 200, 0, 100 and 300 ms, an idle callback, and a button whose command records a line. after() only registers; nothing runs yet.
  2. Before the loop. The last line before mainloop() appends its own entry, which is why it comes first in the printed order.
  3. The loop starts. mainloop() takes over and waits for the next thing that is due.
  4. after(0) first. The timer of 0 ms is due at once, so the loop runs it first.
  5. Then the idle callback. after_idle() runs when nothing else is waiting, which happens straight after the first timer.
  6. The click at 100 ms. The 100 ms timer calls button.invoke(), which runs the button's command exactly as a click would.
  7. The 200 ms timer. At 200 ms the next timer is due and runs. Each callback ran alone; while one runs, the loop handles nothing else, not even redrawing.
  8. quit() at 300 ms. The last timer calls root.quit(), which makes mainloop() return to the code that called it.

Nothing ran until mainloop() started; then the loop ran the callbacks one at a time, in the order their events became due, and quit() made mainloop() return. after() returns an identifier, and after_cancel() with that identifier stops a timer that has not fired yet. One rule follows from the cycle below: while a callback runs, nothing else does, not even redrawing the window. turtle, the drawing module, runs on this same loop.

Cycle: mainloop waits for an event, calls its callback, and redraws the window only after the callback returns.mainloop() waitsfor the next eventAn event arrivesclick, key, timer, redrawTk calls its callbacksplit(), a timer function ...The callback returnsvariables and labels updatedTk redraws what changednothing else runsuntil it returnsnext event

The event loop runs one callback at a time

Text description of the diagram

1. mainloop() waits for the next event: a click, a key press, a timer that is due, or a part of the window that needs to be drawn again. 2. When an event arrives, Tk calls the callback bound to it, such as the Split button's split() method. 3. Nothing else runs until that callback returns: no other clicks are handled and the window is not redrawn. 4. When the callback returns, the variables and labels it changed are up to date, and Tk redraws what changed. 5. The loop goes back to waiting for the next event. A callback that takes two seconds therefore freezes the window for two seconds.

Slow work therefore has to leave the callback, in one of three ways. after() can cut it into short steps that the loop runs between other events. A worker thread can do it while the loop goes on, then hand its result to the window through a queue.Queue. A ProcessPoolExecutor can run heavy computation in other processes, each with its own interpreter.

Checkpoint

Check: The event loop, one callback at a time

  1. Question 1 of 2 In event_order.py, why did "before mainloop()" come first, although it was the last line before mainloop()?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Because after() and after_idle() only register callbacks; the event loop runs them once mainloop() starts

    Scheduling a callback returns at once. Nothing scheduled runs until the event loop runs, and then it runs the callbacks one at a time, in the order their events become due, until quit() makes mainloop() return.

  2. Question 2 of 2 A button's callback downloads a 50 MB file before it returns. What does the person see meanwhile?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A frozen window that does not redraw or answer clicks until the download ends

    The event loop runs one callback at a time. While a slow callback runs, nothing else happens, not even redrawing, so slow work has to happen somewhere other than inside the callback.

Check yourself

Answer the questions on windows, layout and the event loop; the weakest of the three comes up first.

Practice that adapts to you

Practise this lesson

9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 Why does share() live in bill_logic.py, apart from the window?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: So that it can be tested and reused without a window

    The window reads the inputs, calls plain functions and shows the result. The functions can then be tested and reused without any window at all.

  2. Question 2 of 9 The People spinbox holds the text four. What does self.people.get() do, and how does the app cope?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: It raises TclError, which split() catches and shows as a message

    An IntVar that holds no number raises TclError on get(). split() catches it together with ValueError and tells the person to check the input.

  3. Question 3 of 9 On Ubuntu, import tkinter fails with ModuleNotFoundError, although Python itself works. What is the fix?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Install the system package python3-tk, which holds tkinter on Debian and Ubuntu

    The installers from python.org include tkinter on Windows and macOS. On Debian and Ubuntu it is the separate system package python3-tk.

  4. Question 4 of 9 Why should pack() and grid() never place widgets in the same container?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: The two can resize the container against each other until the program hangs

    Use one geometry manager per container. A window may still use both, each in a Frame of its own.

  5. Question 5 of 9 A window needs a toolbar along the top and a work area that fills the rest. Which manager fits best?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: pack, with the toolbar against the top and the work area set to fill and expand

    grid for forms and dialogs, pack for simple stacks such as a toolbar above a work area, and place rarely, for an overlay at an exact spot.

  6. Question 6 of 9 An entry stays narrow when the window is widened, although its column has weight 1. What is missing?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: sticky="ew" on the entry, so that it stretches across its cell from west to east

    Two settings work together: the column's weight decides who gets the extra width, and the widget's sticky decides whether it grows into it.

  7. Question 7 of 9 What does event_order.py print?

    What does this program print? Choose one answer.

    import tkinter as tk
    
    root = tk.Tk()
    root.withdraw()
    order = []
    
    root.after(200, lambda: order.append("after(200): a timer, 200 ms later"))
    root.after(0, lambda: order.append("after(0): a timer that is due at once"))
    root.after_idle(lambda: order.append("after_idle(): when nothing else is waiting"))
    button = tk.Button(root, text="Pay", command=lambda: order.append("the button's command"))
    root.after(100, button.invoke)                     # a click, 100 ms after the loop starts
    root.after(300, root.quit)                         # end mainloop() after 300 ms
    order.append("before mainloop(): nothing above has run yet")
    
    root.mainloop()                                    # runs the callbacks, one at a time, until quit()
    order.append("after mainloop() returned")
    root.destroy()
    print("\n".join(f"{i}. {step}" for i, step in enumerate(order, 1)))
    Show the answer: Question 7 of 9

    Answer: it prints

    1. before mainloop(): nothing above has run yet
    2. after(0): a timer that is due at once
    3. after_idle(): when nothing else is waiting
    4. the button's command
    5. after(200): a timer, 200 ms later
    6. after mainloop() returned

    Registering returns at once; mainloop() then runs each callback when it becomes due, one at a time, until quit().

  8. Question 8 of 9 How do you stop a timer that after() scheduled, before it fires?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: Keep the identifier after() returns and pass it to after_cancel()

    after(ms, func) returns an identifier; after_cancel(identifier) removes the timer, as long as it has not fired.

  9. Question 9 of 9 A callback sets a label to "Saving…", saves for 2 seconds, then sets it to "Saved". The person never sees "Saving…". Why?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: The label is redrawn only when the loop gets a turn, after the callback returns, and by then it says Saved

    Inside a callback nothing is redrawn. Do the save in a worker or in steps with after(), so that the loop can draw "Saving…" before the work starts.

Slow work without a frozen window, measured Pro: opens with a Pro, Premium or Ultimate pass

A callback that works for a second freezes the window for a second: no clicks, no redrawing, a “not responding” title on some systems. This part measures how often the event loop gets a turn during one second of work, done three ways, and sets out the rules for threads in a tkinter program.

In this part:

  • A frozen window measured: no turns for the event loop in one second
  • Three ways to keep the window responsive, compared
  • A progress bar, a Cancel button and errors, through the same queue

1 diagram · 1 runnable example (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Testing a tkinter app without a window on screen Pro: opens with a Pro, Premium or Ultimate pass

A GUI that has only been clicked by hand breaks quietly when someone changes it. This part tests the bill calculator at two levels, the logic alone and the window driven by code, shows why a generated key press does nothing in a hidden window and what to use instead, checks input as it is typed, and ends with an exercise.

In this part:

  • Logic first, then the wiring: eight checks that run in a second
  • Key presses, focus and virtual events
  • Input checked as it is typed, with validatecommand
  • Exercise: the bill calculator’s logic, exact to the paisa
    • The bill calculator's logic, exact to the paisa

2 runnable examples (Python, bill_app.py, bill_logic.py) · 1 exercise with sample tests · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does mainloop() do in a tkinter program, and why does a slow callback freeze the window?

    mainloop() runs Tk's event loop: it waits for the next event, a click, a key press, a timer scheduled with after() or a part of the window that needs drawing, and calls the callback bound to it, then goes back to waiting. It returns only when quit() is called or the window is destroyed. Callbacks run one at a time, in the loop's own thread, and redrawing the window is itself an event. So while a callback runs, nothing else happens: no clicks are handled, no timers fire and the window is not redrawn. A callback that takes two seconds freezes the window for two seconds, which is why callbacks must be short and slow work must happen elsewhere.

  2. Fresher to mid

    A tkinter tool downloads a 200 MB report when the person clicks Fetch, and the window shows "Not Responding" for a minute. Redesign it so that the window stays responsive and shows progress.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A colleague's tkinter app updates a label from a worker thread with label.config(text=...). It works on their laptop, but on another computer the app fails at random. What is wrong, and what do you change?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Your tkinter app has no tests, and every release breaks something in the window. Plan a test suite that runs on every push on a Linux CI server without a display.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A tkinter program builds widgets, places them with a geometry manager, connects callbacks, and then calls mainloop().
  • The window only reads the inputs, calls plain functions and shows the result, so the logic can be tested without a window.
  • Prefer ttk widgets and grid for forms, and never mix pack and grid in one container.
  • The event loop runs one callback at a time and redraws nothing meanwhile, so every callback must be short.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.