Previous | Tutorial index | Next

Tutorial 6: Event Handling and Callback Functions in Tkinter

Learning Objectives

1. Introduction – What Is Event‑Driven Programming?

In traditional procedural programming, you write code that runs from top to bottom, and the program terminates when it finishes. But a GUI application is different—it waits. It waits for the user to do something: click a button, press a key, move the mouse, or close the window.

This paradigm is called event‑driven programming. Your program defines a set of event handlers (functions) and then enters an infinite event loop (mainloop()). The event loop constantly monitors the system for events and dispatches them to the appropriate handlers.

Think of a restaurant:

Tkinter provides three primary mechanisms for handling events:

  1. command – The simplest, used specifically for button‑like widgets.
  2. bind() – The most flexible, attaches any event to any widget.
  3. Variable classes (StringVar, etc.) – Automatically trigger callbacks when values change via trace().

We will explore each in depth.

2. Part 1: The command Option – The Simplest Handler

2.1 How It Works

The command option is available on widgets that are "clickable" or "actionable": Button, Checkbutton, Radiobutton, Menu, Scale, and Spinbox. When the user activates the widget (clicks, selects, or changes it), the function referenced by command is called with no arguments (by default).

2.2 Basic Syntax

def say_hello(): print("Hello, Tkinter!") btn = tk.Button(root, text="Say Hello", command=say_hello) btn.pack()

Crucial: When you assign command=say_hello, you are passing the function object (without parentheses). If you write command=say_hello(), you would call the function immediately at creation time, and the button would do nothing on click.

2.3 Widgets That Support command

Widget When command is triggered
Button On mouse click (left button release).
Checkbutton When the checkbox state is toggled.
Radiobutton When the radio button is selected.
Menu When a menu item is selected.
Scale When the slider is moved (if command is set).
Spinbox When the up/down arrows are clicked or Enter is pressed.

2.4 The state Option and Command Execution

You can disable a button using state=tk.DISABLED. When disabled, the command will not fire.

btn.config(state=tk.DISABLED) # Grayed out, cannot be clicked btn.config(state=tk.NORMAL) # Re‑enable

2.5 The invoke() Method – Programmatic Click

You can simulate a button click programmatically:

btn.invoke() # Calls the command function immediately

3. Part 2: The bind() Method – Universal Event Handling

3.1 Why Do We Need bind()?

command is limited to a small set of widgets and only handles the "activation" event. bind() works with any widget (including root, Frame, Label) and can listen for any event—key presses, mouse movements, focus changes, window resizing, etc.

3.2 Syntax

widget.bind(event_sequence, callback_function)

3.3 The Event Object – What's Inside?

When a bound callback is called, Tkinter passes an Event object with useful attributes:

Attribute Description
event.widget The widget that triggered the event.
event.char The character pressed (for <Key> events).
event.keysym The symbolic name of the key (e.g., "Return", "Escape").
event.keycode The numeric key code.
event.x, event.y Mouse coordinates relative to the widget.
event.x_root, event.y_root Mouse coordinates relative to the screen.
event.num Mouse button number (1 = left, 2 = middle, 3 = right).
event.type The type of event (e.g., "2" for KeyPress).

3.4 Common Event Patterns

Event Pattern Meaning
<Button-1> Left mouse button click.
<Button-2> Middle mouse button click.
<Button-3> Right mouse button click.
<Double-Button-1> Double‑click left mouse button.
<Key> Any key press (returns the character).
<KeyPress-Return> Pressing the Enter/Return key.
<KeyPress-Escape> Pressing the Escape key.
<Control-KeyPress-c> Ctrl+C (or Cmd+C on macOS).
<FocusIn> Widget receives keyboard focus.
<FocusOut> Widget loses keyboard focus.
<Enter> Mouse pointer enters the widget area.
<Leave> Mouse pointer leaves the widget area.
<Motion> Mouse moves inside the widget.
<Configure> Widget is resized or moved.
<Destroy> Widget is about to be destroyed.

3.5 Binding Levels – Order Matters

Tkinter has three levels of binding, in order of precedence (highest to lowest):

  1. Instance bindingwidget.bind(event, handler) – specific to one widget.
  2. Class bindingwidget.bind_class(className, event, handler) – applies to all widgets of a class (e.g., all Entry widgets).
  3. Application bindingroot.bind_all(event, handler) – applies to all widgets in the application, globally.

Crucial: Multiple bindings can fire for the same event. A key press, for example, will first trigger an instance binding, then a class binding, then an application binding.

3.6 Stopping Event Propagation – return "break"

If you want to prevent further handlers from processing the event (e.g., stop an Entry widget from inserting a character), return the string "break" from your callback:

def prevent_input(event): print("No typing allowed!") return "break" # Prevents the character from being inserted entry.bind("<Key>", prevent_input)

3.7 Unbinding – unbind()

To remove a binding, use:

widget.unbind("<Key>") # Removes the binding for that event on this widget

4. Part 3: Tkinter Variable Classes – StringVar, IntVar, DoubleVar, BooleanVar

4.1 What Are They?

These are Tkinter's special "observable" variables. When you change their value using .set(value), any widget that is linked to them via the textvariable, variable, or value options automatically updates its display. This is much more efficient than manually calling .config().

4.2 Creating and Using Variables

name_var = tk.StringVar() # Default value is empty string age_var = tk.IntVar(value=18) # Default value is 18 is_student = tk.BooleanVar(value=True)

4.3 Linking to Widgets

# Link to Entry – changes to the Entry update the variable, and vice versa entry = tk.Entry(root, textvariable=name_var) entry.pack() # Link to Label – shows whatever is in name_var label = tk.Label(root, textvariable=name_var) label.pack() # Link to Checkbutton check_var = tk.IntVar() check = tk.Checkbutton(root, text="Agree", variable=check_var)

4.4 The Most Powerful Feature: trace()

trace() allows you to attach a callback that runs whenever the variable changes. This is incredibly powerful for validation, auto‑completion, and dynamic UI updates.

Syntax:

variable.trace(mode, callback)

Example: Auto‑uppercase

def on_name_change(var_name, var_index, operation): current = name_var.get() name_var.set(current.upper()) # Force uppercase name_var = tk.StringVar() name_var.trace("w", on_name_change) entry = tk.Entry(root, textvariable=name_var) entry.pack()

Caution: Recursive modification (changing the variable inside its own trace callback) can cause infinite loops. Use carefully.

4.5 Removing a Trace

The .trace() method returns a trace ID (a string). You can remove it using .trace_remove(trace_id).

trace_id = name_var.trace("w", callback) # Later: name_var.trace_remove(trace_id)

5. Part 4: Lambda Functions in Callbacks – Passing Arguments

5.1 The Problem

The command option and bind() callbacks are designed to receive no arguments (command) or one argument (the event object). But what if you need to pass custom data to your function?

5.2 The Solution: Lambda Functions

A lambda function acts as a wrapper that calls your function with the required arguments.

def save_file(filename): print(f"Saving {filename}...") btn1 = tk.Button(root, text="Save Report", command=lambda: save_file("report.txt")) btn2 = tk.Button(root, text="Save Data", command=lambda: save_file("data.csv"))

5.3 Important: The "Late Binding" Trap

This is one of the most common bugs in Tkinter. If you create lambda functions inside a loop and capture a loop variable, all lambdas may end up using the last value of that variable.

Buggy Example:

buttons = [] for i in range(3): btn = tk.Button(root, text=f"Button {i}", command=lambda: print(i)) btn.pack() buttons.append(btn) # Clicking any button prints "2" (the last value), not the expected 0, 1, 2!

Why? The lambda function does not evaluate i at the time of creation; it captures a reference to the variable i. By the time you click the button, the loop has finished and i == 2.

The Fix – Default Argument Binding:

for i in range(3): btn = tk.Button(root, text=f"Button {i}", command=lambda val=i: print(val)) btn.pack()

Here, val=i evaluates i immediately and stores its value in the lambda's default argument.

5.4 Using Lambda with bind() – Passing Extra Arguments

Since bind() callbacks receive an event object, you can use a lambda to pass both the event and custom data:

def on_click(event, custom_arg): print(f"Clicked {custom_arg} at x={event.x}") widget.bind("<Button-1>", lambda event: on_click(event, "Hello"))

5.5 Lambda vs. functools.partial

An alternative to lambda is functools.partial, which is sometimes more readable:

from functools import partial btn = tk.Button(root, text="Save", command=partial(save_file, "report.txt"))

6. Part 5: Advanced Event Handling Techniques

6.1 Keyboard Shortcuts (Accelerators)

You can bind global keyboard shortcuts to the root window:

root.bind("<Control-s>", lambda e: save_file("document.txt")) root.bind("<Escape>", lambda e: root.destroy())

On macOS, Command is represented by <Command> or <Meta>.

6.2 Mouse Wheel on Windows and Linux

6.3 Combining Multiple Events

Some widgets (like Listbox) require double‑click detection:

def on_double_click(event): widget = event.widget selection = widget.curselection() if selection: print(f"Selected item: {widget.get(selection[0])}") listbox.bind("<Double-Button-1>", on_double_click)

6.4 Event break – Preventing Default Behavior

Returning "break" stops the event from being processed further. This is useful for custom input validation:

def numeric_only(event): if not event.char.isdigit(): return "break" # Don't insert the character entry.bind("<Key>", numeric_only)

7. Check Your Understanding (Quizzes)

Quiz 1: Multiple Choice

  1. Which of the following widgets does NOT support the command option?
AnswerB – `Label` does not have a `command` option.
  1. A function bound with bind() receives how many arguments?
AnswerB – The callback receives one `Event` object.
  1. What is the correct way to get the character pressed from a keyboard event?
AnswerB – `event.char` gives the character (or empty string for non‑character keys).
  1. Why does the following code print "2" for all buttons when clicked?

    for i in range(3): tk.Button(root, text=str(i), command=lambda: print(i)).pack()
AnswerB – The loop variable `i` ends at 2, and lambda captures it by reference, so all buttons print 2.
  1. Which method is used to stop an event from propagating to other bindings?
AnswerB – `return "break"` prevents further processing.

Quiz 2: True or False

  1. True / False: The command option can be used with an Entry widget to validate text as the user types.
AnswerFalse – `Entry` does not have `command`; you need `bind` or validation.
  1. True / False: Tkinter variable classes (StringVar, IntVar) automatically update their linked widgets without any extra calls.
AnswerTrue – That is the whole point of variable classes.
  1. True / False: A lambda function inside a loop that uses a loop variable will always capture the current value at the time of loop iteration.
AnswerFalse – It captures by reference, so it captures the final value. Use default arguments to fix.
  1. True / False: The <Key> event triggers for modifier keys like Shift and Ctrl.
AnswerTrue – `` fires for all keys. You can check `event.keysym` to distinguish.
  1. True / False: The trace() method on a StringVar can be used to run a callback every time the variable is read ("r" mode).
AnswerTrue – `trace` supports `"r"` mode.

Quiz 3: Fill in the Blanks

  1. To pass arguments to a command callback, you typically use a ________ function.
Answerlambda (or `functools.partial`)
  1. The event pattern for a left‑mouse click is <________-1>.
AnswerButton
  1. The method that programmatically triggers a button's command is .________().
Answerinvoke
  1. To remove a binding, you use .________(event_sequence).
Answerunbind
  1. The three trace modes are "w" (write), "r" (read), and "____" (unset).
Answeru

8. Hands‑On Lab (In‑Class Exercises)

Exercise 1: Command and Lambda Practice

Create a window with three buttons labelled "Red", "Green", and "Blue". When clicked, each button should change the background colour of a central Label to the corresponding colour. Use a single callback function that accepts a colour argument, and use lambda (with default argument binding) to pass the colours.

Sample Solution ```python import tkinter as tk root = tk.Tk() label = tk.Label(root, text="Color me", width=20, bg="white") label.pack(pady=10) def change_color(color): label.config(bg=color) for c in ("Red", "Green", "Blue"): btn = tk.Button(root, text=c, command=lambda col=c: change_color(col)) btn.pack(side=tk.LEFT, padx=5) root.mainloop() ```

Exercise 2: Keyboard Shortcut Dashboard

Create a simple dashboard with a Label that displays the text "Press a key". Bind the <Key> event to the root window. Whenever the user presses a key, update the label to show: "Key pressed: {char} (Keysym: {keysym}, Code: {keycode})". Use the event object to extract the information.

Sample Solution ```python import tkinter as tk root = tk.Tk() label = tk.Label(root, text="Press a key", font=("Arial", 16)) label.pack(expand=True) def on_key(event): label.config(text=f"Key pressed: {event.char} (Keysym: {event.keysym}, Code: {event.keycode})") root.bind("", on_key) root.focus_set() root.mainloop() ```

Exercise 3: Input Validation with trace

Create an Entry that only accepts numeric digits (0‑9). Use a StringVar with a trace("w", callback) that checks the new value. If the new value contains any non‑digit, revert it to the previous valid value (you will need to store the previous value in a separate variable).

Sample Solution ```python import tkinter as tk root = tk.Tk() var = tk.StringVar() prev_value = "" def validate(var_name, idx, mode): global prev_value current = var.get() if current == "": prev_value = "" return if not current.isdigit(): var.set(prev_value) else: prev_value = current var.trace("w", validate) entry = tk.Entry(root, textvariable=var) entry.pack() root.mainloop() ```

Exercise 4: Mouse Events on a Canvas

Draw a circle on a Canvas. Bind <Enter> and <Leave> events to the circle (or the canvas) to change its colour when the mouse hovers over it. Also bind <Button-1> to print the (x, y) coordinates of the click on the canvas.

Sample Solution ```python import tkinter as tk root = tk.Tk() canvas = tk.Canvas(root, width=200, height=200, bg="white") canvas.pack() circle = canvas.create_oval(50, 50, 100, 100, fill="blue") def on_enter(event): canvas.itemconfig(circle, fill="red") def on_leave(event): canvas.itemconfig(circle, fill="blue") def on_click(event): print(f"Clicked at ({event.x}, {event.y})") canvas.tag_bind(circle, "", on_enter) canvas.tag_bind(circle, "", on_leave) canvas.bind("", on_click) root.mainloop() ```

Exercise 5: Dynamic Button Creation (and the Lambda Trap)

Write a loop that creates 5 buttons numbered 1 to 5. Each button, when clicked, should print its own number. You must use a lambda and avoid the late‑binding trap. Test it thoroughly.

Sample Solution ```python import tkinter as tk root = tk.Tk() for i in range(1, 6): btn = tk.Button(root, text=str(i), command=lambda val=i: print(val)) btn.pack(side=tk.LEFT) root.mainloop() ```

9. Homework Assignment

Objective

Demonstrate mastery of event handling by building a semi‑functional GUI application that uses command, bind, variable trace, and lambda functions appropriately.

Part A: Interactive Drawing Tool (20 points)

Build a simple drawing tool on a Canvas (400x400). The app must have:

Requirements:

Sample Solution Outline - Root window with canvas, buttons. - `mode_var = tk.StringVar(value="Draw")`; label showing mode. - `color_list` for random. - `draw(event)` uses mode to create oval: if Draw mode, random color; if Eraser, white. - Bind `` and `` to `draw`. - Bind `` to change color randomly. - Bind `` to root: if 'c' clear, if 'd' toggle mode. - Trace `mode_var` to update label. - Buttons "Clear" and "Exit".

Part B: Debugging Event Handlers (10 points)

The following code is supposed to create a form where typing in the first Entry automatically copies the text to a second Entry (mirroring), but it has three distinct errors. Identify each error, explain the consequence, and provide the corrected code.

import tkinter as tk root = tk.Tk() def copy_text(): # Error 1: Wrong way to get/set text text = entry1.get() entry2.config(text=text) # Error 2: Entry uses textvariable, not text entry1 = tk.Entry(root) entry1.pack() entry2 = tk.Entry(root) entry2.pack() # Error 3: Binding to the wrong event and wrong target entry1.bind("<Button-1>", copy_text) root.mainloop()

Note: Entry widgets use textvariable or .insert()/.delete()/.get(), not .config(text=...).

Answers 1. `entry2.config(text=text)` – wrong; Entry widgets do not have a `text` option; you must use `textvariable` or `insert`/`delete`. Consequence: text will not appear. 2. Binding to `` (mouse click) instead of `` – copy only happens on click, not when typing. 3. Binding to `entry1` instead of `root` or using trace on a variable – better to use a `StringVar` and trace, or bind to `` on entry1.

Corrected code:

import tkinter as tk root = tk.Tk() def copy_text(event): entry2.delete(0, tk.END) entry2.insert(0, entry1.get()) entry1 = tk.Entry(root) entry1.pack() entry2 = tk.Entry(root) entry2.pack() entry1.bind("<KeyRelease>", copy_text) root.mainloop()

(Alternative: use StringVar with trace for cleaner solution.)

Part C: The Late Binding Trap – Analysis and Fix (5 points)

Given the following code that creates a grid of buttons (3x3) representing a numeric keypad, explain why all buttons print 8 when clicked. Then rewrite the loop correctly using either lambda with default arguments or functools.partial.

buttons = [] for row in range(3): for col in range(3): num = row * 3 + col btn = tk.Button(root, text=str(num), command=lambda: print(num)) btn.grid(row=row, column=col) buttons.append(btn)
Answer All buttons print `8` because the lambda captures the variable `num` by reference. When the loop finishes, `num` is 8, so when any button is clicked, it prints 8.

Fix using default argument:

btn = tk.Button(root, text=str(num), command=lambda val=num: print(val))

Or using functools.partial:

from functools import partial btn = tk.Button(root, text=str(num), command=partial(print, num))

Part D: Variable Trace Implementation (5 points)

Write a complete, runnable script that:

Sample Solution ```python import tkinter as tk root = tk.Tk() var = tk.StringVar() entry = tk.Entry(root, textvariable=var) entry.pack() label = tk.Label(root, text="Length: 0") label.pack() def update_length(var_name, idx, mode): label.config(text=f"Length: {len(var.get())}") var.trace("w", update_length) root.mainloop() ```

Part E: Reflection – Why Event‑Driven is Different (5 points)

Write a short essay (7‑10 sentences) explaining the fundamental difference between event‑driven programming and procedural programming. Use the restaurant analogy or create your own. Explain how the mainloop() acts as the "engine" and why command, bind, and trace are all necessary in different situations.

Sample Answer Event‑driven programming and procedural programming differ fundamentally in their control flow. In procedural programming, the code has a predetermined order; it starts, executes each line, and ends. In event‑driven programming, the program does not follow a fixed order – it sits in an infinite loop (the `mainloop`), waiting for external stimuli (events). This is like a TV remote: you don't press buttons in a fixed sequence; you press them as needed, and the TV reacts. The `mainloop` is the engine that listens for events and dispatches them to appropriate handlers. `command` is the simplest way to link a widget to a reaction, `bind` is universal for any event on any widget, and `trace` is for monitoring data changes. Each tool serves a different need: `command` for activation, `bind` for all kinds of interactions, and `trace` for responding to variable modifications without manual checks.

10. Summary of Key Terms (Glossary)

Term Definition
Event An action performed by the user (click, key press, mouse movement) or the system (window resize).
Event Loop (mainloop) The infinite loop that waits for events and dispatches them to handlers.
Callback A function that is called when an event occurs.
command The simplest callback mechanism, available on actionable widgets, called with no arguments.
bind() A method that attaches an event handler to any widget for any event type.
Event Object An object passed to a bind() callback, containing details about the event (coordinates, key, widget, etc.).
break A special return value that stops further processing of an event.
StringVar / IntVar / etc. Tkinter variable classes that synchronise with widgets and support trace().
trace() A method on Tkinter variables that calls a callback when the variable is read, written, or deleted.
Late Binding Trap The phenomenon where lambda functions capture variables by reference, causing all instances to use the last value. Solved by default arguments.
unbind() Removes an event binding from a widget.

11. Further Resources for Self‑Study

This tutorial is designed to take approximately 3 hours of study, lab work, and homework. Event handling is what breathes life into your GUI – mastering it separates a static display from a truly interactive application.

Previous | Tutorial index | Next