2025, Oct 25 07:00
Make a Tkinter Toplevel Modal in Python: Use grab_set and wait_window to stop code until the dialog closes
Learn how to make a Tkinter Toplevel modal with grab_set and wait_window so your Python GUI waits for user input before continuing. Includes code. Examples.
When a Tkinter app spawns a subwindow for user input, the rest of the function may continue running immediately. The result is familiar: the pop-up shows up, but the main code has already executed as if the user had answered. The fix is to make that subwindow modal and explicitly wait for it to close before proceeding.
Problem setup
The main window launches a subwindow where the user picks an option and clicks Done. However, the main function continues execution right after creating the subwindow, so it reads the selection before the user makes one.
import tkinter as tk
class AppShell:
    def __init__(self, master):
        self.master = master
        self.run_btn = tk.Button(master, text='Run', command=lambda: self.execute_flow(master))
        self.run_btn.grid(row=1, column=0)
    def execute_flow(self, parent):
        stage = 2
        if stage == 2:
            ChoiceDialog(parent)  # subwindow is created, but code doesn't wait
        picked = ChoiceDialog.value  # read value too early
        print(f'selected: {picked}')
root = tk.Tk()
AppShell(root)
root.mainloop()class ChoiceDialog:
    value = ''
    def __init__(self, parent):
        self.win = tk.Toplevel(parent)
        self.win.transient(parent)
        self.win.title('User Input')
        self.listbox = tk.Listbox(self.win, selectmode='browse', exportselection=tk.FALSE, relief='groove')
        self.listbox.grid(row=0, column=0)
        self.items = ['Selection1', 'Selection2', 'Selection3']
        for opt in self.items:
            self.listbox.insert(tk.END, opt)
        tk.Button(self.win, text='Done', command=lambda: self.on_done(self.listbox, self.win)).grid(row=1, column=0)
    def on_done(self, lb, toplevel):
        sel = lb.curselection()
        if len(sel) > 0:
            val = lb.get(sel)
            if val == 'Selection1':
                ChoiceDialog.value = 'Selection1'
            elif val == 'Selection2':
                ChoiceDialog.value = 'Selection2'
            elif val == 'Selection3':
                ChoiceDialog.value = 'Selection3'
            toplevel.destroy()
        else:
            ChoiceDialog.value = 'None'
            toplevel.destroy()
            print('No selection made')What actually happens and why
The subwindow is created, but nothing in the main flow tells Tkinter to pause until that subwindow is closed. Tkinter proceeds to the next lines immediately, meaning the code tries to read the selection before the user clicks Done. The visible symptom is that the pop-up appears after the rest of the function has already run.
Solution: make the subwindow modal and wait
Two calls solve this. First, block interaction with the main window so the user can’t spawn multiple subwindows: use grab_set on the subwindow. Second, explicitly wait until the subwindow is closed: call wait_window with that subwindow. You can perform these inside the dialog’s initializer so the call site remains clean.
import tkinter as tk
class AppShellFixed:
    def __init__(self, master):
        self.master = master
        self.run_btn = tk.Button(master, text='Run', command=self.execute_flow)
        self.run_btn.grid(row=1, column=0)
    def execute_flow(self):
        stage = 2
        if stage == 2:
            dlg = ChoiceDialogModal(self.master)  # creation blocks until closed
            picked = dlg.selection                 # guaranteed to be set now
            print(f'selected: {picked}')
root = tk.Tk()
AppShellFixed(root)
root.mainloop()class ChoiceDialogModal:
    def __init__(self, parent):
        self.top = tk.Toplevel(parent)
        self.selection = None
        self.lb = tk.Listbox(self.top)
        self.lb.grid(row=0, column=0)
        for opt in ['Selection1', 'Selection2', 'Selection3']:
            self.lb.insert(tk.END, opt)
        tk.Button(self.top, text='Done', command=self.handle_done).grid(row=1, column=0)
        self.top.grab_set()                         # block access to main window
        self.top.master.wait_window(self.top)       # wait until subwindow is closed
    def handle_done(self):
        picked = self.lb.curselection()
        if len(picked) > 0:
            self.selection = self.lb.get(picked)
        else:
            self.selection = None
            print('No selection made')
        self.top.destroy()Details that improve the flow
Waiting on the subwindow ensures the main function resumes only after the user completes input. Using grab_set prevents the user from interacting with the parent window during that time, which also avoids launching the same dialog twice. Storing the result on the dialog instance keeps state local and easy to access, and calling methods and reading widgets via self avoids passing widgets around. Adopting CamelCaseNames for classes like ChoiceDialogModal improves readability and aligns naming with Tkinter classes such as Frame and Button. If a prebuilt interaction suits the need, Tkinter’s Dialog options from the standard documentation can be considered as well.
Why this matters
In GUI code, sequencing is everything. Without explicit waiting, logic that depends on user input runs with incomplete state, leading to wrong branches, empty values, or repeated prompts. Making the subwindow modal and waiting for it to close gives deterministic control flow and a cleaner user experience.
Conclusion
To pause execution until a Tkinter subwindow returns a user choice, create a Toplevel, call grab_set to block the main window, and wait_window to suspend the caller until the subwindow is destroyed. Keep the selection as an instance attribute and access widgets via self for clarity. This pattern ensures the rest of your function runs only after the user has made a selection, exactly when you need it.
The article is based on a question from StackOverflow by Juliette Mitrovich and an answer by furas.