Free Ebook cover Desktop Apps with Tkinter: A Beginner’s Guide to Python GUIs

Desktop Apps with Tkinter: A Beginner’s Guide to Python GUIs

New course

10 pages

Event Handling in Tkinter: Commands, bind(), and Keyboard/Mouse Events

Capítulo 5

Estimated reading time: 9 minutes

+ Exercise

How Tkinter Reacts to User Actions

Tkinter is event-driven: your code sets up widgets and then enters the main event loop. While the loop runs, Tkinter waits for events (button clicks, key presses, mouse movement, focus changes) and calls your callback functions when those events occur. “Wiring interactions correctly” means attaching the right callback to the right event source and using the correct callback signature.

Two main ways to handle events

  • Widget options like command=: simple, high-level callbacks for widgets such as Button and menu items.
  • bind(): lower-level event binding for keyboard, mouse, focus, and many other events; callbacks receive an event object.

Command Callbacks (Button, Menu)

command callbacks are the simplest: you pass a function object, and Tkinter calls it with no arguments when the user activates the widget.

Correct vs. common pitfall: passing vs. calling

The most common mistake is writing command=save(), which calls the function immediately during setup. You must pass the function itself: command=save.

import tkinter as tk

root = tk.Tk()

def save():
    print("Saving...")

btn_ok = tk.Button(root, text="Save", command=save)   # correct
# btn_bad = tk.Button(root, text="Save", command=save())  # wrong: runs now

btn_ok.pack()
root.mainloop()

Menu commands work the same way

Menu items also use command and therefore expect a zero-argument callback.

import tkinter as tk

root = tk.Tk()

def new_file():
    print("New file")

def quit_app():
    root.destroy()

menubar = tk.Menu(root)
file_menu = tk.Menu(menubar, tearoff=False)
file_menu.add_command(label="New", command=new_file)
file_menu.add_separator()
file_menu.add_command(label="Quit", command=quit_app)
menubar.add_cascade(label="File", menu=file_menu)

root.config(menu=menubar)
root.mainloop()

Passing extra data to a command callback (lambda done safely)

Because command callbacks take no parameters, you often wrap your function with lambda (or functools.partial) to supply arguments. The key is: the wrapper must be a function that runs later, not a function call that runs now.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

import tkinter as tk

root = tk.Tk()

def set_mode(mode):
    print("Mode:", mode)

tk.Button(root, text="Edit", command=lambda: set_mode("edit")).pack()
tk.Button(root, text="View", command=lambda: set_mode("view")).pack()

root.mainloop()

Late-binding pitfall in loops: if you create many buttons in a loop, capture the loop variable using a default argument.

import tkinter as tk

root = tk.Tk()

def choose(n):
    print("Chose", n)

for n in range(1, 4):
    tk.Button(root, text=f"Option {n}", command=lambda n=n: choose(n)).pack()

root.mainloop()

bind(): Keyboard, Mouse, Focus, and More

bind() attaches a callback to a specific event pattern. Unlike command, a bind callback is called with one argument: an event object that contains useful information (which key, mouse position, which widget triggered it, etc.).

Callback signature for bind

Bind callbacks should accept event:

def on_key(event):
    print("Key pressed:", event.keysym)

If you want to call a function that doesn’t need the event, wrap it:

widget.bind("<Return>", lambda event: submit())

Common event patterns

  • Keyboard: <KeyPress>, <KeyRelease>, <Return>, <Escape>
  • Mouse: <Button-1> (left click), <Button-3> (right click), <Double-Button-1>, <Motion>
  • Focus: <FocusIn>, <FocusOut>

Widget-level vs. application-level bindings

  • widget.bind(...): only triggers when that widget has focus (for keyboard) or receives the mouse event.
  • root.bind(...): binds to the root window; still often depends on focus.
  • root.bind_all(...): catches events anywhere in the app (useful for global shortcuts like Ctrl+S).

Using the event object

Some commonly used fields:

  • event.widget: the widget that triggered the event
  • event.keysym: symbolic key name (e.g., Return, s)
  • event.char: typed character (may be empty for special keys)
  • event.x, event.y: mouse position relative to the widget
  • event.x_root, event.y_root: mouse position relative to the screen

Enter-to-Submit Behavior

A common GUI behavior is pressing Enter to submit a form or trigger an action. You can implement this by binding <Return> on the relevant widget (often an Entry) or globally.

import tkinter as tk

root = tk.Tk()

entry = tk.Entry(root)
entry.pack(padx=10, pady=10)

status = tk.Label(root, text="Type and press Enter")
status.pack(padx=10, pady=(0, 10))

def submit():
    status.config(text=f"Submitted: {entry.get()}")

entry.bind("<Return>", lambda event: submit())

root.mainloop()

Notice the wrapper: submit() takes no event, but the bind callback receives one, so we use lambda event: submit().

Guided Build: Shortcuts, Click-to-Focus, and a Status Label

This guided build wires multiple event types into one small app: a text area, a status label that updates on events, Ctrl+S to “save”, click-to-focus behavior, and Enter-to-submit (for a simple command entry).

Step 1: Create the UI skeleton

We’ll use a text widget for content, an entry for a quick command line, and a status label at the bottom. The status label will be updated by event handlers.

import tkinter as tk

root = tk.Tk()
root.title("Event Handling Demo")

text = tk.Text(root, width=60, height=15)
text.pack(padx=10, pady=(10, 5), fill="both", expand=True)

cmd_entry = tk.Entry(root)
cmd_entry.pack(padx=10, pady=5, fill="x")

status = tk.Label(root, text="Ready", anchor="w")
status.pack(padx=10, pady=(5, 10), fill="x")

root.mainloop()

Step 2: Add a status update helper

Centralize status updates so every handler can call the same function.

def set_status(message):
    status.config(text=message)

Step 3: Implement Ctrl+S (global shortcut)

Use bind_all so the shortcut works even when focus is in the text widget or entry. On many systems, the event pattern for Ctrl+S is <Control-s>. The callback receives an event object.

def save_shortcut(event):
    content = text.get("1.0", "end-1c")
    set_status(f"Ctrl+S pressed: {len(content)} chars (pretend saved)")
    return "break"

Returning "break" stops further processing of the event. This is useful when you don’t want the key press to also insert a character or trigger another binding.

Bind it:

root.bind_all("<Control-s>", save_shortcut)

Step 4: Click-to-focus behavior

Mouse clicks can be used to explicitly move focus. While Tkinter often focuses widgets automatically, it’s common to enforce focus for a smoother experience (especially if you have frames or custom clickable areas).

def focus_text(event):
    text.focus_set()
    set_status("Text focused (clicked)")

def focus_cmd(event):
    cmd_entry.focus_set()
    set_status("Command entry focused (clicked)")

text.bind("<Button-1>", focus_text)
cmd_entry.bind("<Button-1>", focus_cmd)

Note on focus events: you can also update status when focus changes, without relying on mouse clicks.

def on_focus_in(event):
    set_status(f"Focus in: {event.widget.__class__.__name__}")

def on_focus_out(event):
    set_status(f"Focus out: {event.widget.__class__.__name__}")

text.bind("<FocusIn>", on_focus_in)
text.bind("<FocusOut>", on_focus_out)
cmd_entry.bind("<FocusIn>", on_focus_in)
cmd_entry.bind("<FocusOut>", on_focus_out)

Step 5: Enter-to-submit from the command entry

Pressing Enter in the command entry will “submit” the command. This demonstrates adapting a no-argument function to a bind callback.

def run_command():
    cmd = cmd_entry.get().strip()
    if not cmd:
        set_status("No command to run")
        return
    set_status(f"Command submitted: {cmd}")
    cmd_entry.delete(0, "end")

cmd_entry.bind("<Return>", lambda event: run_command())

Step 6: Update status on typing and mouse movement (optional but instructive)

These bindings show how to read information from the event object.

def on_text_key(event):
    set_status(f"Typing in text: keysym={event.keysym}")

def on_text_motion(event):
    set_status(f"Mouse over text: x={event.x}, y={event.y}")

text.bind("<KeyRelease>", on_text_key)
text.bind("<Motion>", on_text_motion)

Full guided-build code (assembled)

import tkinter as tk

root = tk.Tk()
root.title("Event Handling Demo")

text = tk.Text(root, width=60, height=15)
text.pack(padx=10, pady=(10, 5), fill="both", expand=True)

cmd_entry = tk.Entry(root)
cmd_entry.pack(padx=10, pady=5, fill="x")

status = tk.Label(root, text="Ready", anchor="w")
status.pack(padx=10, pady=(5, 10), fill="x")

def set_status(message):
    status.config(text=message)

def save_shortcut(event):
    content = text.get("1.0", "end-1c")
    set_status(f"Ctrl+S pressed: {len(content)} chars (pretend saved)")
    return "break"

def focus_text(event):
    text.focus_set()
    set_status("Text focused (clicked)")

def focus_cmd(event):
    cmd_entry.focus_set()
    set_status("Command entry focused (clicked)")

def on_focus_in(event):
    set_status(f"Focus in: {event.widget.__class__.__name__}")

def on_focus_out(event):
    set_status(f"Focus out: {event.widget.__class__.__name__}")

def run_command():
    cmd = cmd_entry.get().strip()
    if not cmd:
        set_status("No command to run")
        return
    set_status(f"Command submitted: {cmd}")
    cmd_entry.delete(0, "end")

def on_text_key(event):
    set_status(f"Typing in text: keysym={event.keysym}")

def on_text_motion(event):
    set_status(f"Mouse over text: x={event.x}, y={event.y}")

# Global shortcut
root.bind_all("<Control-s>", save_shortcut)

# Click-to-focus
text.bind("<Button-1>", focus_text)
cmd_entry.bind("<Button-1>", focus_cmd)

# Focus events
text.bind("<FocusIn>", on_focus_in)
text.bind("<FocusOut>", on_focus_out)
cmd_entry.bind("<FocusIn>", on_focus_in)
cmd_entry.bind("<FocusOut>", on_focus_out)

# Enter-to-submit
cmd_entry.bind("<Return>", lambda event: run_command())

# Extra event feedback
text.bind("<KeyRelease>", on_text_key)
text.bind("<Motion>", on_text_motion)

text.focus_set()
root.mainloop()

Practical Pitfalls and How to Avoid Them

1) Mixing up signatures: command vs. bind

  • command expects func() with no parameters.
  • bind expects func(event).

If you have a function that doesn’t need the event, wrap it: lambda event: func().

2) Calling instead of passing

  • Wrong: command=save()
  • Right: command=save
  • Wrong: widget.bind("<Return>", submit())
  • Right: widget.bind("<Return>", submit) if submit accepts event, otherwise lambda event: submit()

3) Lambda in loops (late binding)

Capture loop variables with defaults: lambda event, i=i: handler(i) or for commands lambda i=i: handler(i).

4) Forgetting to stop default behavior when needed

Some key bindings should prevent the default widget behavior. For example, if you bind Ctrl+S globally, returning "break" can prevent the key press from also being processed elsewhere.

Now answer the exercise about the content:

You want Ctrl+S to work as a global “save” shortcut anywhere in a Tkinter app, and you also want to prevent the key press from triggering other default behaviors. Which approach best fits this requirement?

You are right! Congratulations, now go to the next page

You missed! Try again.

bind_all catches events across the app, and bind callbacks receive an event parameter. Returning "break" stops further processing so the key press doesn’t also trigger other behavior.

Next chapter

Menus, Toolbars, and Status Bars in Tkinter Desktop Interfaces

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.