From f60b7976a09fe06eab57dd1e3ed11154ac3ffa93 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 7 Jul 2026 10:16:16 +0300 Subject: [PATCH 1/3] gh-145578: Add tkinter support for the Tk 9.1 accessibility API Add the tk_set_acc_* and tk_get_acc_* families of methods which set and return the accessible attributes of a widget, the tk_add_acc_object(), tk_emit_selection_change() and tk_emit_focus_change() methods which register custom widgets with the platform accessibility API, and the tk_check_screenreader() method which reports whether a screen reader is running. Co-Authored-By: Claude Opus 4.8 --- Doc/library/tkinter.rst | 69 +++++++++++++++ Doc/whatsnew/3.16.rst | 12 +++ Lib/test/test_tkinter/test_misc.py | 29 +++++++ Lib/tkinter/__init__.py | 86 +++++++++++++++++++ ...-07-06-14-38-02.gh-issue-145578.aX11yQ.rst | 9 ++ 5 files changed, 205 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 653a91c5cbedba1..1644029936aafa5 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -2023,6 +2023,75 @@ Base and mixin classes .. versionadded:: 3.16 + The following methods support assistive technologies such as screen + readers. + They require Tk 9.1 or newer. + Tk automatically exposes the standard widgets to the platform + accessibility API; + these methods allow overriding the exposed attributes and registering + custom widgets. + If no assistive technology is active when the application starts, + all these calls (except :meth:`!tk_check_screenreader`) + have no effect and return ``0``. + + .. method:: tk_check_screenreader() + + Return whether a screen reader is currently running. + + .. versionadded:: next + + .. method:: tk_set_acc_role(value) + tk_set_acc_name(value) + tk_set_acc_description(value) + tk_set_acc_value(value) + tk_set_acc_state(value) + tk_set_acc_action(value) + tk_set_acc_help(value) + + Set an accessible attribute of the widget. + The supported roles are ``Button``, ``Canvas``, ``Checkbutton``, + ``Combobox``, ``Entry``, ``Label``, ``Listbox``, ``Menu``, + ``Notebook``, ``Progressbar``, ``Scale``, ``Scrollbar``, + ``Spinbox``, ``Table``, ``Text`` and ``Tree``. + + .. versionadded:: next + + .. method:: tk_get_acc_role() + tk_get_acc_name() + tk_get_acc_description() + tk_get_acc_value() + tk_get_acc_state() + tk_get_acc_action() + tk_get_acc_help() + + Return an accessible attribute of the widget. + Raise :exc:`~tkinter.TclError` if the attribute has not been set. + + .. versionadded:: next + + .. method:: tk_add_acc_object() + + Register the widget with the platform accessibility API. + This is only needed for custom widgets; + the standard widgets are registered automatically. + + .. versionadded:: next + + .. method:: tk_emit_selection_change() + + Notify the platform accessibility API that the selection in the + widget has changed. + + .. versionadded:: next + + .. method:: tk_emit_focus_change() + + Notify the platform accessibility API that the widget has received + focus. + Not available on macOS. + + .. versionadded:: next + The methods with the ``busy_`` prefix manage the busy state of a window, which shows a busy cursor and ignores user input. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 4af5d3c0bd072e2..99f7f372adce767 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -812,6 +812,18 @@ tkinter native print dialog. It requires Tk 8.7/9.0 or newer. (Contributed by Serhiy Storchaka in :gh:`153256`.) +* Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the + :meth:`~tkinter.Misc.tk_set_acc_role` and + :meth:`~tkinter.Misc.tk_get_acc_role` families of methods which set and + return the accessible attributes of a widget, the + :meth:`~tkinter.Misc.tk_add_acc_object`, + :meth:`~tkinter.Misc.tk_emit_selection_change` and + :meth:`~tkinter.Misc.tk_emit_focus_change` methods which register custom + widgets with the platform accessibility API, and the + :meth:`~tkinter.Misc.tk_check_screenreader` method which reports whether + a screen reader is running. + (Contributed by Serhiy Storchaka in :gh:`145578`.) + * Added new window-management methods :meth:`~tkinter.Misc.winfo_isdark` (dark mode detection), :meth:`~tkinter.Wm.wm_iconbadge` (application icon badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order). diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 59769b7250d76ac..3fabc2a3f0a72ac 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -712,6 +712,35 @@ def test_tk_print(self): self.assertNotHasAttr(tkinter.Frame, 'tk_print') self.assertNotHasAttr(tkinter.Misc, 'tk_print') + @requires_tk(9, 1) + def test_accessibility(self): + # Without an active screen reader every "tk accessible" call is + # a no-op returning 0, so only test that the calls do not raise + # and check the values when a screen reader is active. + try: + sr = self.root.tk_check_screenreader() + except tkinter.TclError: + # Tk 9.1a0 or Tk compiled without accessibility support + # (e.g. without ATK on X11). + self.skipTest('the "tk accessible" command is not supported') + self.assertIsInstance(sr, bool) + f = tkinter.Frame(self.root) + for attr in ('name', 'description', 'value', 'state', + 'action', 'help'): + with self.subTest(attr=attr): + getattr(f, f'tk_set_acc_{attr}')('spam') + result = getattr(f, f'tk_get_acc_{attr}')() + if sr: + self.assertEqual(result, 'spam') + f.tk_set_acc_role('Label') + result = f.tk_get_acc_role() + if sr: + self.assertEqual(result, 'Label') + f.tk_add_acc_object() + f.tk_emit_selection_change() + if self.root._windowingsystem != 'aqua': + f.tk_emit_focus_change() + def test_wait_variable(self): var = tkinter.StringVar(self.root) self.assertEqual(self.root.waitvar, self.root.wait_variable) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index f06807a6cb9c7d7..1dab5010cac9941 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -821,6 +821,92 @@ def tk_inactive(self, reset=False, *, displayof=0): else: return self.tk.getint(self.tk.call(args)) + # Accessibility support for assistive technologies such as screen + # readers. Requires Tk 9.1 or newer. If no assistive technology + # is active when the application starts, all these calls (except + # tk_check_screenreader()) have no effect and return 0. + + def tk_check_screenreader(self): + """Return whether a screen reader is currently running. + + Requires Tk 9.1 or newer.""" + return self.tk.getboolean(self.tk.call( + 'tk', 'accessible', 'check_screenreader')) + + def tk_set_acc_role(self, value): + """Set the accessible role of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_role', self._w, value) + + def tk_get_acc_role(self): + """Return the accessible role of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_role', self._w) + + def tk_set_acc_name(self, value): + """Set the accessible name of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_name', self._w, value) + + def tk_get_acc_name(self): + """Return the accessible name of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_name', self._w) + + def tk_set_acc_description(self, value): + """Set the accessible description of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_description', + self._w, value) + + def tk_get_acc_description(self): + """Return the accessible description of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_description', + self._w) + + def tk_set_acc_value(self, value): + """Set the accessible value of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_value', self._w, value) + + def tk_get_acc_value(self): + """Return the accessible value of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_value', self._w) + + def tk_set_acc_state(self, value): + """Set the accessible state of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_state', self._w, value) + + def tk_get_acc_state(self): + """Return the accessible state of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_state', self._w) + + def tk_set_acc_action(self, value): + """Set the accessible action of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_action', self._w, value) + + def tk_get_acc_action(self): + """Return the accessible action of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_action', self._w) + + def tk_set_acc_help(self, value): + """Set the accessible help text of the widget.""" + self.tk.call('tk', 'accessible', 'set_acc_help', self._w, value) + + def tk_get_acc_help(self): + """Return the accessible help text of the widget.""" + return self.tk.call('tk', 'accessible', 'get_acc_help', self._w) + + def tk_add_acc_object(self): + """Register the widget with the platform accessibility API.""" + self.tk.call('tk', 'accessible', 'add_acc_object', self._w) + + def tk_emit_selection_change(self): + """Notify the platform accessibility API that the selection + in the widget has changed.""" + self.tk.call('tk', 'accessible', 'emit_selection_change', self._w) + + def tk_emit_focus_change(self): + """Notify the platform accessibility API that the widget has + received focus. + + Not available on macOS.""" + self.tk.call('tk', 'accessible', 'emit_focus_change', self._w) + def wait_variable(self, name): """Wait until the variable is modified. diff --git a/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst b/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst new file mode 100644 index 000000000000000..8b2eda1d71eb31d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst @@ -0,0 +1,9 @@ +Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the +``tk_set_acc_*`` and ``tk_get_acc_*`` families of methods which set and +return the accessible attributes of a widget, the +:meth:`~tkinter.Misc.tk_add_acc_object`, +:meth:`~tkinter.Misc.tk_emit_selection_change` and +:meth:`~tkinter.Misc.tk_emit_focus_change` methods which register custom +widgets with the platform accessibility API, and the +:meth:`~tkinter.Misc.tk_check_screenreader` method which reports whether a +screen reader is running. From 33f310a111fe1aaaa7c0e2eee06ec099d5d4408a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 6 Nov 2026 14:48:56 +0200 Subject: [PATCH 2/3] Set the accessible role first in the test Setting the role creates the table in which other attributes are stored, so setting any other attribute before it fails. This only shows up when a screen reader is running, because otherwise every "tk accessible" call is a no-op. Co-Authored-By: Claude Opus 5 --- Lib/test/test_tkinter/test_misc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 3fabc2a3f0a72ac..243d80299630ca8 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -725,6 +725,12 @@ def test_accessibility(self): self.skipTest('the "tk accessible" command is not supported') self.assertIsInstance(sr, bool) f = tkinter.Frame(self.root) + # The role has to be set first, it creates the table in which + # other attributes are stored. + f.tk_set_acc_role('Label') + result = f.tk_get_acc_role() + if sr: + self.assertEqual(result, 'Label') for attr in ('name', 'description', 'value', 'state', 'action', 'help'): with self.subTest(attr=attr): @@ -732,10 +738,6 @@ def test_accessibility(self): result = getattr(f, f'tk_get_acc_{attr}')() if sr: self.assertEqual(result, 'spam') - f.tk_set_acc_role('Label') - result = f.tk_get_acc_role() - if sr: - self.assertEqual(result, 'Label') f.tk_add_acc_object() f.tk_emit_selection_change() if self.root._windowingsystem != 'aqua': From d53da15029d24291f6d823c105ca12d2726eb4ae Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 6 Nov 2026 14:53:04 +0200 Subject: [PATCH 3/3] Document that a widget needs an accessible role Without a role, setting or reading any other accessible attribute raises TclError when a screen reader is running. Co-Authored-By: Claude Opus 5.5 --- Doc/library/tkinter.rst | 4 ++++ Lib/tkinter/__init__.py | 1 + 2 files changed, 5 insertions(+) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 1644029936aafa5..6406d2db945c088 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -2049,6 +2049,10 @@ Base and mixin classes tk_set_acc_help(value) Set an accessible attribute of the widget. + A widget must have a role before any other attribute is set or read, + otherwise :exc:`~tkinter.TclError` is raised. + Tk sets the role of most standard widgets when they are mapped; + for other widgets, or before that, call :meth:`!tk_set_acc_role` first. The supported roles are ``Button``, ``Canvas``, ``Checkbutton``, ``Combobox``, ``Entry``, ``Label``, ``Listbox``, ``Menu``, ``Notebook``, ``Progressbar``, ``Scale``, ``Scrollbar``, diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 1dab5010cac9941..92fa2b23a34e12b 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -825,6 +825,7 @@ def tk_inactive(self, reset=False, *, displayof=0): # readers. Requires Tk 9.1 or newer. If no assistive technology # is active when the application starts, all these calls (except # tk_check_screenreader()) have no effect and return 0. + # A widget must have a role before other attributes can be used. def tk_check_screenreader(self): """Return whether a screen reader is currently running.