Events#

Every tkinter app is event-driven: your code runs in callbacks that fire in response to clicks, keystrokes, and higher-level notifications. The Events & callbacks foundations page covers the essentials — command, bind, and after. This guide goes deeper into the binding system: how an event finds its callbacks, how to share and stop bindings, and how to define and dispatch your own events.

Binding scope#

widget.bind attaches a callback to one widget. But every event actually consults four levels of bindings in turn, and understanding them is what lets you write shortcuts and shared behavior instead of wiring every widget by hand:

  • Instancewidget.bind(pattern, callback) — this one widget only.

  • Classwidget.bind_class("TEntry", pattern, callback) — every widget of that Tk class at once. This is the level a widget’s default behavior lives at (what makes an Entry insert the character you type).

  • Toplevel — bindings on the containing window, seen by any widget in it.

  • Applicationwidget.bind_all(pattern, callback) — every widget in the program. The right tool for a global shortcut like <F1> or <Control-q>.

A widget’s class name for bind_class is widget.winfo_class()"TEntry", "TButton", "Text", and so on.

The order events travel#

When an event fires, tkinter walks those four levels in order and runs every matching binding it finds. That order is the widget’s bindtags:

entry.bindtags()
# ('.!entry', 'TEntry', '.', 'all')
#   instance    class   toplevel  application

Two consequences worth internalizing:

  • Your instance binding runs before the class binding — which is why returning "break" from it can pre-empt the widget’s default behavior (see Stopping an event).

  • A matching binding at any level fires, so one bind_all("<F1>", …) covers the whole application.

You can reorder or insert tags with widget.bindtags((...)) for advanced cases, but the default order serves almost everything.

Adding vs replacing#

Within a single tag, binding a pattern replaces whatever callback was already bound to that same pattern — a real footgun when two parts of your code each assume they own the event:

widget.bind("<Button-1>", first)
widget.bind("<Button-1>", second)             # first is silently gone
widget.bind("<Button-1>", third, add="+")     # third runs alongside second

Pass add="+" to add a callback instead of replacing it; every bound callback then fires, in the order added. Reach for it whenever a handler should coexist with others — especially the app-wide events under Generating events, where each consumer must add its own handler without clobbering the rest.

bind returns an id that unbind uses to remove a single callback again:

handler_id = widget.bind("<Motion>", track)
widget.unbind("<Motion>", handler_id)

Stopping an event#

A widget’s class binding implements its default behavior — a Text widget inserting the character you typed, for instance. Because the instance binding runs first, returning the string "break" from your callback halts the chain, so the class binding never runs and the default is suppressed:

def tab_moves_focus(event):
    event.widget.tk_focusNext().focus()
    return "break"        # stop Text from inserting a literal tab

text.bind("<Tab>", tab_moves_focus)

Virtual events#

Virtual events are named, higher-level notifications written in double angle brackets, <<Like this>>. They fire when something meaningful happens rather than for a raw input, and you bind them exactly like physical events. ttkbootstrap emits two you will use:

  • <<ThemeChanged>> — fires on every widget after the theme is switched. Bind it to recolor anything you drew yourself (a Canvas, a custom asset):

    canvas.bind("<<ThemeChanged>>", lambda e: repaint(canvas))
    
  • <<LocaleChanged>> — fires when the app’s locale changes; this is what drives LocaleVar (see Variables & reactivity).

Prefer the callback helpers for theme changes

For rebuilding a custom style after a theme switch, ttkbootstrap offers on_theme_change() and the @theme_aware decorator, which are easier than a raw <<ThemeChanged>> binding. See Custom styles.

Generating events#

You can fire an event yourself with event_generate — most usefully your own virtual events. A virtual event lets one part of an app announce that something happened without knowing who cares: a producer generates it, and any number of consumers bind to it. That decouples components cleanly.

# a producer announces on the root — it calls no one directly
def load_data():
    ...  # do the work
    app.event_generate("<<DataLoaded>>")

# consumers register on the SAME widget, each with add="+" so they stack
app.bind("<<DataLoaded>>", lambda e: status.configure(text="Loaded"), add="+")
app.bind("<<DataLoaded>>", lambda e: table.refresh(), add="+")

The name in double brackets is all you need — a <<virtual>> event works the moment you bind and generate it.

Important

event_generate dispatches to one widget (and its bindtags); it does not broadcast. Bind consumers on the same widget you generate on — the root makes a natural app-wide bus — or use bind_all for a truly global signal. Use add="+" so each consumer adds a handler rather than replacing the previous one.

event_generate also synthesizes physical events, though it’s less common — a bare <KeyRelease> needs the widget focused and the relevant fields for that event type supplied (keysym for keys, x/y for pointer events) to behave like real input.

To make a virtual event fire from the keyboard too, alias it to physical key sequences with event_add:

app.event_add("<<Save>>", "<Control-s>", "<Control-S>")
app.bind("<<Save>>", lambda e: save())   # Ctrl-S now fires <<Save>>

Note

event_generate accepts data="…", but tkinter does not surface it on the event the callback receives, so it cannot be read back. To pass information with a virtual event, store it where both sides can reach it. See the event object.

See also