2025, Nov 17 19:00
Tkinter: Handle User File Selection with Events, Not Pauses, and Initialize Player Safely
Stop blocking Tkinter's main loop. See how to handle user file selection with events, then initialize Config and Player safely instead of 'waiting' for input.
Building GUI logic that waits for the user to decide before proceeding is a common stumbling block in Tkinter. The temptation is to “pause” the application until a choice is made, but that approach fights the event loop and causes exactly the kind of race you’re seeing: code that relies on a value runs before the user has set it. The good news is that the fix is straightforward once you align with Tkinter’s event-driven model.
Problem
Consider a simple client where the user selects a game directory. If there’s exactly one player file utilX.dat inside, the code extracts X as player_id and proceeds. If there are multiple files, a small chooser UI appears to let the user select which one to use. The issue is that the Player object is created immediately after the directory selection, even when the user hasn’t picked a file yet, so player_id is still None.
import tkinter as tk
from tkinter import Menu, Label, Button, Listbox, messagebox
from tkinter import filedialog, ttk
from pathlib import Path
import sys
from player import Player
from config import Config
class ClientApp:
def __init__(self, window):
self.window = window
self.window.title("My Game Client")
self.base_dir = None
self.candidate_files = None
self._picked_idx = ''
self.chosen_file = None
self.app_conf = None
self.profile = None
self.user_id = None
self.build_menu()
print('chosen_file', self.chosen_file) # for illustration only
def build_menu(self):
self.menubar = Menu(self.window)
self.window.config(menu=self.menubar)
self.game_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Game", menu=self.game_menu)
self.game_menu.add_command(label="Select game directory", command=self.pick_game_dir)
self.game_menu.add_separator()
self.game_menu.add_command(label="Save game")
self.game_menu.add_command(label="Inventory viewer", command=self.show_inventory)
self.game_menu.add_command(label="Exit", command=self.window.quit)
def pick_game_dir(self):
while True:
self.base_dir = filedialog.askdirectory()
if not self.base_dir:
if messagebox.askyesno(
icon='warning',
title="Game directory selection",
message="You haven't chosen a game directory. Do you want to quit?"):
sys.exit(0)
else:
p = Path(self.base_dir)
self.candidate_files = list(p.glob('util[0-9].dat'))
if not self.candidate_files:
messagebox.showerror(
title='Directory error',
message='Player files not found in directory')
else:
if len(self.candidate_files) > 1:
names = [f.name for f in self.candidate_files]
self.show_file_picker(names)
else:
self.chosen_file = self.candidate_files[0]
self.user_id = self.chosen_file.stem[-1]
print('self.user_id', self.user_id)
break
# Problem: this runs even if the user still hasn't chosen a file
self.game_menu.entryconfigure('Select game directory', state=tk.DISABLED)
self.app_conf = Config(self.base_dir)
self.profile = Player(self.base_dir, self.user_id) # user_id may be None here
def on_confirm_pick(self):
self._picked_idx = self._listbox.curselection()
self.chosen_file = self.candidate_files[self._picked_idx[0]]
self.user_id = self.chosen_file.stem[-1]
print('self.user_id in on_confirm_pick', self.user_id)
self.chooser_panel.destroy()
def show_file_picker(self, file_names):
self.chooser_panel = ttk.Frame(self.window, padding=(2, 2, 2, 2), name='chooser_panel')
self.chooser_panel.pack()
lbl = ttk.Label(self.chooser_panel, text="Several player files were found. Please choose one.", width=60)
lbl.pack()
var = tk.StringVar(value=file_names)
self._listbox = Listbox(self.chooser_panel, listvariable=var, height=6, width=20)
self._listbox.selection_set(0)
self._listbox.pack()
btn = ttk.Button(self.chooser_panel, text='Select', command=self.on_confirm_pick)
btn.pack()
def show_inventory(self):
if self.chosen_file:
self.profile = Player(self.base_dir)
else:
messagebox.showerror(
title='Player file error',
message='No player file selected. Please select a game directory first.')
if __name__ == "__main__":
root = tk.Tk()
app = ClientApp(root)
root.mainloop()
What’s really happening
The code after the while loop assumes a valid player_id exists, but in the multi-file scenario the Listbox dialog is created and control returns immediately to the mainloop. The user hasn’t clicked anything yet, so user_id is still None. Instantiating Player at that moment fails because the prerequisite wasn’t met.
“Never block the main loop.” Don’t try to stall Tkinter with additional mainloop calls, quit(), or modal tricks to simulate sequential console I/O. Let events drive the flow and run the next step from the event handlers.
Attempts like using Toplevel with grab_set() or juggling mainloop()/quit() won’t solve the ordering problem. A normal GUI doesn’t “wait for input before continuing”; it reacts to input and then continues from the corresponding handler.
Solution
Move the dependent logic into a function that you call only after user_id is known. In practice, extract the part that disables the menu, loads Config, and creates Player into a dedicated method. Call it immediately in the single-file case and from the Select button handler in the multi-file case. Remove the unconditional calls that run too early.
import tkinter as tk
from tkinter import Menu, Label, Button, Listbox, messagebox
from tkinter import filedialog, ttk
from pathlib import Path
import sys
from player import Player
from config import Config
class ClientApp:
def __init__(self, window):
self.window = window
self.window.title("My Game Client")
self.base_dir = None
self.candidate_files = None
self._picked_idx = ''
self.chosen_file = None
self.app_conf = None
self.profile = None
self.user_id = None
self.build_menu()
def build_menu(self):
self.menubar = Menu(self.window)
self.window.config(menu=self.menubar)
self.game_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Game", menu=self.game_menu)
self.game_menu.add_command(label="Select game directory", command=self.pick_game_dir)
self.game_menu.add_separator()
self.game_menu.add_command(label="Save game")
self.game_menu.add_command(label="Inventory viewer", command=self.show_inventory)
self.game_menu.add_command(label="Exit", command=self.window.quit)
def activate_player(self):
self.game_menu.entryconfigure('Select game directory', state=tk.DISABLED)
self.app_conf = Config(self.base_dir)
self.profile = Player(self.base_dir, self.user_id)
def pick_game_dir(self):
while True:
self.base_dir = filedialog.askdirectory()
if not self.base_dir:
if messagebox.askyesno(
icon='warning',
title="Game directory selection",
message="You haven't chosen a game directory. Do you want to quit?"):
sys.exit(0)
else:
p = Path(self.base_dir)
self.candidate_files = list(p.glob('util[0-9].dat'))
if not self.candidate_files:
messagebox.showerror(
title='Directory error',
message='Player files not found in directory')
else:
if len(self.candidate_files) > 1:
names = [f.name for f in self.candidate_files]
self.show_file_picker(names)
else:
self.chosen_file = self.candidate_files[0]
self.user_id = self.chosen_file.stem[-1]
print('self.user_id', self.user_id)
self.activate_player() # run only when user_id is ready
break
def on_confirm_pick(self):
self._picked_idx = self._listbox.curselection()
self.chosen_file = self.candidate_files[self._picked_idx[0]]
self.user_id = self.chosen_file.stem[-1]
print('self.user_id in on_confirm_pick', self.user_id)
self.activate_player() # run only when user_id is ready
self.chooser_panel.destroy()
def show_file_picker(self, file_names):
self.chooser_panel = ttk.Frame(self.window, padding=(2, 2, 2, 2), name='chooser_panel')
self.chooser_panel.pack()
lbl = ttk.Label(self.chooser_panel, text="Several player files were found. Please choose one.", width=60)
lbl.pack()
var = tk.StringVar(value=file_names)
self._listbox = Listbox(self.chooser_panel, listvariable=var, height=6, width=20)
self._listbox.selection_set(0)
self._listbox.pack()
btn = ttk.Button(self.chooser_panel, text='Select', command=self.on_confirm_pick)
btn.pack()
def show_inventory(self):
if self.chosen_file:
self.profile = Player(self.base_dir)
else:
messagebox.showerror(
title='Player file error',
message='No player file selected. Please select a game directory first.')
if __name__ == "__main__":
root = tk.Tk()
app = ClientApp(root)
root.mainloop()
Why this matters
In GUI applications, users can trigger actions at any time. Code that assumes a specific timing will eventually break. By placing dependent work into the event handlers that set the required state, you ensure the program runs only when the prerequisites are true. It also keeps the interface responsive because nothing blocks the main loop, and it avoids brittle hacks like nested mainloops or modal grabs for control flow.
Takeaway
Think in terms of events, not pauses. When a user action sets a value such as player_id, immediately call the next step from that handler. If the path splits, branch there and then, as in calling activate_player right after determining that there’s only one player file or after the user confirms a choice. This pattern keeps logic correct, the UI fluid, and the flow easy to reason about.