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.
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:
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- Read and hand over. split() reads the three variables with get() and calls share('1499', 4, 10). It does no arithmetic itself.
- The answer. share() adds the tip and rounds each person's share up to a whole rupee, so it returns 413.
- 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.
- 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.
BillAppis attk.Frameholding the widgets. Each input is tied to a tkinter variable:StringVarorIntVarobjects whoseget()andset()read and change what the widget shows.ttk.Button(..., command=self.split)names the callback, andinvoke()calls it exactly as a click does, which is how--self-testused the window without anyone clicking.split()catchesValueErrorfrom the logic andTclErrorfrom anIntVarthat 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
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:
| Criterion | grid | pack | place |
|---|---|---|---|
| How it places widgets | in rows and columns of a table | one after another against a side: top, bottom, left or right | at coordinates you give, absolute or relative |
| Resizing | columns and rows grow by their weight | fill and expand options | only what you compute |
| Typical use | forms, dialogs, most windows | a toolbar above a work area | an overlay at an exact spot |
| When to choose | Most windows | Simple stacks | Rarely |
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:
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
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():
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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- Before the loop. The last line before mainloop() appends its own entry, which is why it comes first in the printed order.
- The loop starts. mainloop() takes over and waits for the next thing that is due.
- after(0) first. The timer of 0 ms is due at once, so the loop runs it first.
- Then the idle callback. after_idle() runs when nothing else is waiting, which happens straight after the first timer.
- The click at 100 ms. The 100 ms timer calls button.invoke(), which runs the button's command exactly as a click would.
- 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.
- 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.
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
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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.
-
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?
-
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.
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
ttkwidgets andgridfor forms, and never mixpackandgridin one container. - The event loop runs one callback at a time and redraws nothing meanwhile, so every callback must be short.
References
- tkinter: Python interface to Tcl/Tk (Python Software Foundation)
- tkinter: threading model (Python Software Foundation)
- tkinter.ttk: Tk themed widgets (Python Software Foundation)
- turtle: turtle graphics (Python Software Foundation)
- decimal: decimal fixed-point and floating-point arithmetic (Python Software Foundation)
- queue: a synchronized queue class (Python Software Foundation)
- Tk 9.0: the grid geometry manager (Tcl Core Team)
- Tk 9.0: event, virtual events and event generation (Tcl Core Team)
- Tk 9.0: entry, validation (Tcl Core Team)
- Pyodide: Python standard library, removed modules (The Pyodide project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress