2025, Nov 22 23:00

Fixing Tkinter virtual events that don't reach parent bindings: target the right widget and schedule with after

Learn why Tkinter virtual events from nested widgets don't trigger parent bindings, how event_generate works, and how to fix timing with after in main loop.

When a Tkinter widget generates a virtual event inside its own constructor and the handler is bound on a parent, nothing fires. The event appears to “stay local” to the nested widget. This guide shows why that happens and how to make the event reach the intended handler without redesigning the entire GUI.

Reproducing the issue

The example below creates a frame with an inner frame. The outer frame defines a virtual event and binds a handler to it. The inner frame emits that event during initialization.

import tkinter as tk
app = tk.Tk()
class Container(tk.Frame):
    class Inner(tk.Frame):
        def __init__(self, host) -> None:
            tk.Frame.__init__(self, host)
            self.event_generate("<<Ping>>", when="tail")
    def __init__(self, host) -> None:
        tk.Frame.__init__(self, host)
        self.event_add("<<Ping>>", "<Button-1>")
        self.bind("<<Ping>>", self.on_ping)
        self.inner = self.Inner(self)
        self.inner.grid(column=0, row=0)
    def on_ping(self, event):
        print("event occured")
ui = Container(app)
ui.grid(column=0, row=0)
app.mainloop()

What is actually happening

In Tkinter, a generated event targets a specific widget. It does not bubble to parents or children except in one special case described below. The default routing for an event sent to a widget is processed in this order: first, the widget’s own instance bindings; next, the widget’s class bindings (for example, Button for a button); then the owning toplevel’s instance bindings (not applicable when the widget itself is a toplevel); and finally, the global all bindings. Parent frames are not in that chain, nor are child widgets.

That explains why binding on the outer frame and generating on the inner frame produces no output. The inner frame receives the event, but it has no matching instance binding, so nothing happens at that level. The routing then checks the class, the toplevel, and all, none of which match your instance binding on the parent frame.

There is a second subtlety here. Emitting the event inside __init__ can occur before mainloop() is running, so the event queue isn’t ready to process it. That timing detail can also prevent your handler from running.

The fix

The event must be sent to the widget that owns the binding. If the outer frame needs to handle the virtual event, the inner frame should target the outer frame when generating it. When the generation happens during construction, schedule it so the main loop can actually process the event.

import tkinter as tk
app = tk.Tk()
class Container(tk.Frame):
    class Inner(tk.Frame):
        def __init__(self, host) -> None:
            tk.Frame.__init__(self, host)
            self.after(100, lambda: host.event_generate("<<Ping>>", when="tail"))
    def __init__(self, host) -> None:
        tk.Frame.__init__(self, host)
        self.event_add("<<Ping>>", "<Button-1>")
        self.bind("<<Ping>>", self.on_ping)
        self.inner = self.Inner(self)
        self.inner.grid(column=0, row=0)
    def on_ping(self, event):
        print("event occured")
ui = Container(app)
ui.grid(column=0, row=0)
app.mainloop()

Two key changes make the behavior reliable. First, host.event_generate targets the parent frame that owns the handler. Second, after delays the generation so the main loop can process it. The optional when="tail" keeps the event ordering consistent with the original intent.

Why this knowledge matters

Understanding Tkinter’s event routing saves time and prevents brittle workarounds. Binding directly on the widget that should handle the event is strongly recommended. Overriding binding tags to redirect events more broadly is possible, but it’s tricky and can have complex side effects. Choosing the proper target widget and scheduling event generation at the right time results in predictable, maintainable GUI code.

Takeaways

If a virtual event isn’t invoking your handler, first check where you bound the handler and where you generate the event. Send the event to the correct widget instead of relying on implicit propagation. If you trigger events during widget construction, use after so the main loop can process them. With those two adjustments, the event system behaves consistently and your handlers run where you expect.