Building Interfaces with tkinter
Windows, widgets, layout and events. Turning a console program into something with buttons that a real person would use.
Your first window
import tkinter as tk
window = tk.Tk()
window.title("My First App")
window.geometry("400x300")
label = tk.Label(window, text="Hello, world!", font=("Arial", 16))
label.pack(pady=20)
window.mainloop()What each line does
tk.Tk()creates the main windowtitle()sets the text in the title bargeometry("400x300")sets the width and height in pixelstk.Label(window, ...)creates a label inside that windowpack()places the widget on screen. Nothing appears until it is placed.mainloop()starts the event loop, which waits for the user to do something. Everything after it runs only when the window closes.
A widget you never pack, grid or place will not appear. This is the single most common beginner problem with tkinter.
Widgets and layout
The widgets you will use most
tk.Label(window, text="Some text")
tk.Button(window, text="Click me", command=do_something)
tk.Entry(window, width=30) # single line text box
tk.Text(window, height=5, width=40) # multi line
tk.Checkbutton(window, text="Agree", variable=agreed)
tk.Radiobutton(window, text="Red", variable=colour, value="red")
tk.Listbox(window)
tk.Scale(window, from_=0, to=100, orient="horizontal")
tk.Frame(window) # a container for groupingThree layout managers
pack stacks widgets in order.
label.pack(side="top", pady=10)
button.pack(side="bottom", fill="x")grid arranges widgets in rows and columns. This is the one to use for forms.
tk.Label(window, text="Name:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
name_entry = tk.Entry(window)
name_entry.grid(row=0, column=1, padx=5, pady=5)
tk.Label(window, text="Age:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
age_entry = tk.Entry(window)
age_entry.grid(row=1, column=1, padx=5, pady=5)
tk.Button(window, text="Submit").grid(row=2, column=0, columnspan=2, pady=10)place positions by exact pixel coordinates. Avoid it, because the layout breaks the moment the window is resized.
tkinter will freeze with no error message. Use one or the other per frame.
Reading and writing widget values
name = name_entry.get() # read an Entry
name_entry.delete(0, tk.END) # clear it
name_entry.insert(0, "default") # put text in
label.config(text="Updated!") # change a Label after creationVariables that update automatically
score = tk.IntVar(value=0)
message = tk.StringVar(value="Ready")
tk.Label(window, textvariable=message).pack()
def add_point():
score.set(score.get() + 1)
message.set(f"Score: {score.get()}")Changing the variable updates every widget bound to it, with no manual refresh.
- A Label shows text.
- An Entry lets the user type.
- A Button runs a function when clicked.
- A Listbox shows a list.
- mainloop() waits for events. A click calls your function, the screen is redrawn, then it waits again.
- Grid is a table. Every widget gets a row and a column, counted from 0.
- One grid() call per widget, and columnspan lets a widget cross several columns.
- pack, grid and place are the three layout managers. Use only one of them inside a container.
Events and a complete app
Buttons
def on_click():
print("Clicked")
tk.Button(window, text="Go", command=on_click).pack()Note there are no brackets after on_click. You are passing the function itself, not calling it.
Passing arguments
tk.Button(window, text="Add 5", command=lambda: add_score(5)).pack()Binding other events
window.bind("<Return>", lambda event: submit())
window.bind("<Key>", on_keypress)
canvas.bind("<Button-1>", on_left_click)Message boxes
from tkinter import messagebox
messagebox.showinfo("Saved", "Your work has been saved.")
messagebox.showerror("Error", "That is not a valid number.")
if messagebox.askyesno("Confirm", "Delete this record?"):
delete_record()A complete application
import tkinter as tk
from tkinter import messagebox
class MarkBookApp:
def __init__(self, root):
self.root = root
self.root.title("Mark Book")
self.root.geometry("420x420")
self.marks = []
self.build_interface()
def build_interface(self):
form = tk.Frame(self.root, padx=12, pady=12)
form.pack(fill="x")
tk.Label(form, text="Student name:").grid(row=0, column=0, sticky="e", pady=4)
self.name_entry = tk.Entry(form, width=22)
self.name_entry.grid(row=0, column=1, pady=4)
tk.Label(form, text="Mark out of 100:").grid(row=1, column=0, sticky="e", pady=4)
self.mark_entry = tk.Entry(form, width=22)
self.mark_entry.grid(row=1, column=1, pady=4)
tk.Button(form, text="Add", width=12, command=self.add_mark)\
.grid(row=2, column=0, columnspan=2, pady=8)
self.listbox = tk.Listbox(self.root, height=10)
self.listbox.pack(fill="both", expand=True, padx=12)
self.summary = tk.StringVar(value="No marks yet")
tk.Label(self.root, textvariable=self.summary, pady=8).pack()
self.root.bind("<Return>", lambda event: self.add_mark())
def add_mark(self):
name = self.name_entry.get().strip()
entry = self.mark_entry.get().strip()
if name == "":
messagebox.showerror("Missing name", "Please enter a student name.")
return
if not entry.isdigit() or not 0 <= int(entry) <= 100:
messagebox.showerror("Invalid mark", "Enter a whole number between 0 and 100.")
return
mark = int(entry)
self.marks.append(mark)
self.listbox.insert(tk.END, f"{name:<20} {mark}")
average = sum(self.marks) / len(self.marks)
self.summary.set(f"{len(self.marks)} marks, average {average:.1f}, highest {max(self.marks)}")
self.name_entry.delete(0, tk.END)
self.mark_entry.delete(0, tk.END)
self.name_entry.focus()
root = tk.Tk()
app = MarkBookApp(root)
root.mainloop()This program validates every input, gives clear error messages, updates its summary automatically and supports the enter key. That is the standard to aim for.
Key terms you must be able to define
- tkinter
- Python's standard library for building graphical user interfaces.
- Widget
- A single interface component such as a label, button or text box.
- Window
- The top level container created with Tk(), holding all the widgets.
- mainloop
- The event loop that waits for user actions and keeps the window open.
- pack
- A layout manager that stacks widgets in the order they are added.
- grid
- A layout manager that places widgets in rows and columns.
- Event
- Something the user does, such as a click or a key press, which the program responds to.
- command
- The Button option naming the function to call when the button is pressed.
- StringVar
- A tkinter variable that automatically updates any widget bound to it.
Flashcards
Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.
Reach the top grade on this topic
- Use grid for anything form shaped, and never mix grid and pack in one container
- Pass the function name without brackets to command, and use lambda when arguments are needed
- Validate every Entry before using its value, since get() always returns a string
- Use StringVar and IntVar so labels update themselves
- Structure a larger app as a class, so widgets are attributes rather than globals
Mistakes that cost marks here
- Creating a widget but never calling pack or grid, so it never appears.
- Writing command=on_click() with brackets, which calls the function immediately instead of on click.
- Mixing pack and grid in the same container, which freezes the program with no error.
- Forgetting that Entry.get() returns a string, then comparing it with a number.
- Putting code after mainloop() and wondering why it never runs.
tkinter has been part of Python's standard library since 1994, which means it is already installed on almost every computer running Python. No pip install, no dependencies, nothing to go wrong on the school network.
Knowledge check
window.mainloop() do?tk.Button(window, command=save())?entry.get() return?label.config(text="New") do?Exam-style questions
Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.
- LIt starts the event loop
- LIt keeps the window open and waits for user actions such as clicks and key presses
- LCode after it does not run until the window closes
The mainloop method starts tkinter's event loop, which is what makes the interface interactive. It keeps the window on screen and continuously waits for events such as button clicks, key presses and mouse movements, dispatching each one to whatever function has been bound to it. Because the program stays inside mainloop until the window is closed, any code written after the mainloop call will not run while the application is open, which is a common source of confusion for beginners.
label = tk.Label(window, text="Hello") but nothing appears when the program runs. Explain why and give the correction.2 marks- LThe widget has not been added to the window with a layout manager
- LAdd label.pack() or label.grid() to display it
Creating a widget builds it in memory but does not put it on screen. A layout manager must be called to add it to the window, so the student needs to add a line such as label.pack() or label.grid(row=0, column=0) after creating the label. Without this the label exists but is never displayed.
- Lpack stacks widgets in the order they are added
- Lgrid places widgets in rows and columns
- Lpack suits simple stacked layouts
- Lgrid suits forms where labels and inputs must line up
The pack layout manager stacks widgets one after another in the order they are added, using a side such as top, bottom, left or right. It is quick and suits simple layouts where widgets sit in a single column or row, such as a title, a list and a status bar down the window. The grid layout manager places widgets in numbered rows and columns like a table, which makes it far better for forms where a column of labels must line up neatly against a column of entry boxes. Grid also allows a widget to span several columns and to be aligned within its cell. The two managers must never be used within the same container, because tkinter will hang without producing an error message.
- LImports tkinter and creates the main window
- LCreates an Entry widget and displays it
- LCreates a Label to show the result
- LDefines a function that reads the entry
- LUpdates the label using config or a StringVar
- LAttaches the function to the button with command and starts mainloop
import tkinter as tk def show_text(): result.config(text="You typed: " + entry.get()) window = tk.Tk() window.title("Echo") entry = tk.Entry(window, width=30) entry.pack(pady=10) tk.Button(window, text="Show", command=show_text).pack() result = tk.Label(window, text="") result.pack(pady=10) window.mainloop() The function is passed to command without brackets, so it is called when the button is clicked rather than immediately when the program starts. The function reads the current contents of the entry with get and updates the label using config. Every widget is packed, otherwise none of them would appear.
- LEntry.get() always returns a string, whatever the user typed
- LThe user may type letters or leave it blank when a number is expected
- LWithout validation the program may crash or produce incorrect results
The get method of an Entry widget always returns a string, no matter what the user typed, so any value intended as a number must be converted before it can be used in a calculation. Users can type anything at all into an entry box, including letters, symbols or nothing, so if the program calls int on the contents without checking it first, a ValueError will be raised and the application will crash. Validating the input, for example by checking isdigit and then checking the value falls within an acceptable range, allows the program to display a clear message asking the user to correct the entry instead, which is what makes the application robust.