From 5cf12d86a36878f4d83ef4efd3f74eb28777a38e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 27 Jun 2026 19:02:45 +0300 Subject: [PATCH 1/2] gh-103878: Return a consistent empty value from cancelled file dialogs On cancellation Tcl may report the empty result as '', () or b'' depending on the platform and Tk version. Normalize it so that askopenfilename(), asksaveasfilename() and askdirectory() always return '' and askopenfilenames() always returns (). Open._fixresult() now distinguishes single from multiple results by the 'multiple' option rather than the result type. Co-authored-by: Christopher Chavez Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_tkinter/test_filedialog.py | 33 +++++++++++++++++++ Lib/tkinter/filedialog.py | 17 ++++++---- ...-06-27-19-02-17.gh-issue-103878.216fa7.rst | 7 ++++ 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst diff --git a/Lib/test/test_tkinter/test_filedialog.py b/Lib/test/test_tkinter/test_filedialog.py index 054e719a0f883d9..758ef1479ef2cfa 100644 --- a/Lib/test/test_tkinter/test_filedialog.py +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -37,6 +37,39 @@ def test_directory(self): self.check(filedialog.Directory, 'tk_chooseDirectory') +class CancelResultTest(AbstractTkTest, unittest.TestCase): + # On cancellation Tcl may report the empty result as '', () or b'' + # (gh-103878). _fixresult() normalizes it to the documented empty value: + # '' for the filename dialogs and () for the multiple-selection dialog. + + def check(self, dialog, expected): + for empty in ('', (), b''): + with self.subTest(empty=empty): + result = dialog._fixresult(self.root, empty) + self.assertEqual(result, expected) + self.assertIs(type(result), type(expected)) + + def test_open(self): + self.check(filedialog.Open(self.root), '') + + def test_saveas(self): + self.check(filedialog.SaveAs(self.root), '') + + def test_directory(self): + self.check(filedialog.Directory(self.root), '') + + def test_openfilenames(self): + self.check(filedialog.Open(self.root, multiple=1), ()) + + def test_results_preserved(self): + # A real selection is returned unchanged. + single = filedialog.Open(self.root) + self.assertEqual(single._fixresult(self.root, '/a/spam'), '/a/spam') + multiple = filedialog.Open(self.root, multiple=1) + self.assertEqual(multiple._fixresult(self.root, ('/a', '/b')), + ('/a', '/b')) + + class FileDialogTest(AbstractTkTest, unittest.TestCase): # The pure-Python FileDialog runs its own modal loop in go(); its logic is # exercised here without entering the loop. diff --git a/Lib/tkinter/filedialog.py b/Lib/tkinter/filedialog.py index e2eff98e601c07c..8c9bb8cd496579c 100644 --- a/Lib/tkinter/filedialog.py +++ b/Lib/tkinter/filedialog.py @@ -311,7 +311,9 @@ def _fixoptions(self): pass def _fixresult(self, widget, result): - if result: + if not result: + result = '' # normalize the cancelled result (gh-103878) + else: # keep directory and filename until next time # convert Tcl path objects to strings try: @@ -335,17 +337,16 @@ class Open(_Dialog): command = "tk_getOpenFile" def _fixresult(self, widget, result): - if isinstance(result, tuple): - # multiple results: + if self.options.get("multiple"): + # multiple results: a tuple of filenames + if not isinstance(result, tuple): + result = widget.tk.splitlist(result) result = tuple([getattr(r, "string", r) for r in result]) if result: path, file = os.path.split(result[0]) self.options["initialdir"] = path # don't set initialfile or filename, as we have multiple of these return result - if not widget.tk.wantobjects() and "multiple" in self.options: - # Need to split result explicitly - return self._fixresult(widget, widget.tk.splitlist(result)) return _Dialog._fixresult(self, widget, result) @@ -362,7 +363,9 @@ class Directory(commondialog.Dialog): command = "tk_chooseDirectory" def _fixresult(self, widget, result): - if result: + if not result: + result = '' # normalize the cancelled result (gh-103878) + else: # convert Tcl path objects to strings try: result = result.string diff --git a/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst b/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst new file mode 100644 index 000000000000000..7a0ce2e2936aef4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst @@ -0,0 +1,7 @@ +The :mod:`tkinter.filedialog` functions that return a filename +(:func:`~tkinter.filedialog.askopenfilename`, +:func:`~tkinter.filedialog.asksaveasfilename` and +:func:`~tkinter.filedialog.askdirectory`) now consistently return an empty +string when the dialog is cancelled, instead of an empty tuple or ``b''`` +on some platforms. :func:`~tkinter.filedialog.askopenfilenames` likewise +always returns an empty tuple. From 6694f0288529758e945b4081188974dfb0e236d6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 29 Jun 2026 23:22:05 +0300 Subject: [PATCH 2/2] gh-103878: Make askopenfiles() return a list consistently askopenfiles() returned a list of open files normally but an empty tuple when cancelled. Make it always return a list, empty when cancelled, as its docstring already describes. Update the file dialog documentation accordingly, now that the empty value returned on cancellation is consistent for each function. Co-authored-by: Christopher Chavez Co-Authored-By: Claude Opus 4.8 --- Doc/library/dialog.rst | 8 +++----- Lib/tkinter/filedialog.py | 7 +------ .../2026-06-27-19-02-17.gh-issue-103878.216fa7.rst | 3 ++- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Doc/library/dialog.rst b/Doc/library/dialog.rst index c3f62117ed61d78..a576cdcfaa2b1e9 100644 --- a/Doc/library/dialog.rst +++ b/Doc/library/dialog.rst @@ -159,10 +159,8 @@ listed below: The below functions when called create a modal, native look-and-feel dialog, wait for the user's selection, and return it. The exact return value depends on the function (see below); when the dialog is -cancelled it is an empty string, an empty tuple or ``None``. -The precise type of this empty value may vary between platforms and Tk -versions, so test the result for truth rather than comparing it with a -specific value. +cancelled it is the empty value documented for that function -- an empty +string, an empty tuple, an empty list or ``None``. .. function:: askopenfile(mode="r", **options) askopenfiles(mode="r", **options) @@ -171,7 +169,7 @@ specific value. :func:`askopenfile` returns the opened file object, or ``None`` if the dialog is cancelled. :func:`askopenfiles` returns a list of the opened file objects, or an empty - tuple if cancelled. + list if cancelled. The files are opened in mode *mode* (read-only ``'r'`` by default). .. function:: asksaveasfile(mode="w", **options) diff --git a/Lib/tkinter/filedialog.py b/Lib/tkinter/filedialog.py index 8c9bb8cd496579c..23cb19249a92835 100644 --- a/Lib/tkinter/filedialog.py +++ b/Lib/tkinter/filedialog.py @@ -423,12 +423,7 @@ def askopenfiles(mode = "r", **options): """ files = askopenfilenames(**options) - if files: - ofiles=[] - for filename in files: - ofiles.append(open(filename, mode)) - files=ofiles - return files + return [open(filename, mode) for filename in files] def asksaveasfile(mode = "w", **options): diff --git a/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst b/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst index 7a0ce2e2936aef4..dd5b8c2545fe653 100644 --- a/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst +++ b/Misc/NEWS.d/next/Library/2026-06-27-19-02-17.gh-issue-103878.216fa7.rst @@ -4,4 +4,5 @@ The :mod:`tkinter.filedialog` functions that return a filename :func:`~tkinter.filedialog.askdirectory`) now consistently return an empty string when the dialog is cancelled, instead of an empty tuple or ``b''`` on some platforms. :func:`~tkinter.filedialog.askopenfilenames` likewise -always returns an empty tuple. +always returns an empty tuple, and :func:`~tkinter.filedialog.askopenfiles` +an empty list.