From fd7a29eb23d1bc7df32f343bc9e18e96c2e97bb7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 7 Jul 2026 10:04:22 +0300 Subject: [PATCH] gh-145578: Add tkinter support for the Tk 9.1 accessibility API The accessible property of widgets returns an Accessible object whose attributes are exposed to assistive technologies such as screen readers and whose methods register custom widgets with the platform accessibility API. The tk_check_screenreader() method reports whether a screen reader is running. Co-Authored-By: Claude Opus 4.8 --- Doc/library/tkinter.rst | 70 ++++++++++++++++ Doc/whatsnew/3.16.rst | 9 +++ Lib/test/test_tkinter/test_misc.py | 36 +++++++++ Lib/tkinter/__init__.py | 80 +++++++++++++++++++ ...-07-06-14-38-02.gh-issue-145578.aX11yQ.rst | 7 ++ 5 files changed, 202 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 ca034fcdc09b43..fb97c12e48059d 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -1894,6 +1894,31 @@ Base and mixin classes .. versionadded:: next + The following members 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; + the :attr:`!accessible` attributes allow overriding what is exposed, + and the :class:`Accessible` methods allow registering custom widgets. + If no assistive technology is active when the application starts, + setting the attributes has no effect, + reading them returns ``0``, + and the methods do nothing (except :meth:`!tk_check_screenreader`). + + .. method:: tk_check_screenreader() + + Return whether a screen reader is currently running. + + .. versionadded:: next + + .. attribute:: accessible + + The accessible attributes of the widget, + as an :class:`Accessible` object. + + .. versionadded:: next + The methods with the ``busy_`` prefix manage the busy state of a window, which shows a busy cursor and ignores user input. @@ -2323,6 +2348,51 @@ Base and mixin classes .. versionadded:: 3.11 +.. class:: Accessible() + + The accessible attributes of a widget, + returned by the :attr:`~Misc.accessible` property of the widget. + The attributes are exposed to assistive technologies + such as screen readers. + Requires Tk 9.1 or newer. + + Setting an attribute overrides what Tk exposes for the widget; + reading an attribute that has not been set raises + :exc:`~tkinter.TclError`. + The supported roles are ``Button``, ``Canvas``, ``Checkbutton``, + ``Combobox``, ``Entry``, ``Label``, ``Listbox``, ``Menu``, + ``Notebook``, ``Progressbar``, ``Scale``, ``Scrollbar``, + ``Spinbox``, ``Table``, ``Text`` and ``Tree``. + + .. versionadded:: next + + .. attribute:: role + name + description + value + state + action + help + + The accessible attributes of the widget. + + .. method:: add_object() + + Register the widget with the platform accessibility API. + This is only needed for custom widgets; + the standard widgets are registered automatically. + + .. method:: emit_selection_change() + + Notify the platform accessibility API that the selection in the + widget has changed. + + .. method:: emit_focus_change() + + Notify the platform accessibility API that the widget has received + focus. + Not available on macOS. + .. class:: Wm() diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index e8c75d2f571061..d5b01aa57090e3 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -358,6 +358,15 @@ tkinter report the user idle time. (Contributed by Serhiy Storchaka in :gh:`151881`.) +* Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the + :attr:`~tkinter.Misc.accessible` property of widgets returns an + :class:`~tkinter.Accessible` object whose attributes are exposed to + assistive technologies such as screen readers and whose methods register + custom widgets with the platform accessibility API, and the + :meth:`~tkinter.Misc.tk_check_screenreader` method 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 5f75b8d245b3e9..500854ecc3e5c9 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -599,6 +599,42 @@ def test_tk_inactive(self): # Resetting the timer returns None and does not raise. self.assertIsNone(self.root.tk_inactive(reset=True)) + @requires_tk(9, 1) + def test_accessible(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) + acc = f.accessible + self.assertIsInstance(acc, tkinter.Accessible) + self.assertIs(f.accessible, acc) + if sr: + # Reading an attribute that has not been set raises TclError. + with self.assertRaises(tkinter.TclError): + acc.help + for attr in ('name', 'description', 'value', 'state', + 'action', 'help'): + with self.subTest(attr=attr): + setattr(acc, attr, 'spam') + result = getattr(acc, attr) + if sr: + self.assertEqual(result, 'spam') + acc.role = 'Label' + result = acc.role + if sr: + self.assertEqual(result, 'Label') + acc.add_object() + acc.emit_selection_change() + if self.root._windowingsystem != 'aqua': + acc.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 83d11a7695ffbe..06927e7e03f54e 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -818,6 +818,25 @@ def tk_inactive(self, reset=False, *, displayof=0): else: return self.tk.getint(self.tk.call(args)) + 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')) + + @property + def accessible(self): + """The accessible attributes of the widget, as an Accessible + object. + + Requires Tk 9.1 or newer.""" + try: + return self._accessible_object + except AttributeError: + self._accessible_object = Accessible(self) + return self._accessible_object + def wait_variable(self, name): """Wait until the variable is modified. @@ -2307,6 +2326,67 @@ def yview_scroll(self, number, what): self.tk.call(self._w, 'yview', 'scroll', number, what) +def _accessible_attribute(name): + """Internal function. Create an Accessible attribute property.""" + def getter(self): + widget = self._widget + return widget.tk.call('tk', 'accessible', 'get_acc_' + name, + widget._w) + def setter(self, value): + widget = self._widget + widget.tk.call('tk', 'accessible', 'set_acc_' + name, + widget._w, value) + return property(getter, setter, + doc=f'The accessible {name} of the widget.') + + +class Accessible: + """The accessible attributes of a widget. + + Returned by the accessible property of a widget. The attributes + are exposed to assistive technologies such as screen readers. + Requires Tk 9.1 or newer. + + If no assistive technology is active when the application starts, + setting an attribute has no effect and reading it returns 0. + Reading an attribute that has not been set raises TclError. + """ + + def __init__(self, widget): + self._widget = widget + + def __repr__(self): + return f'' + + role = _accessible_attribute('role') + name = _accessible_attribute('name') + description = _accessible_attribute('description') + value = _accessible_attribute('value') + state = _accessible_attribute('state') + action = _accessible_attribute('action') + help = _accessible_attribute('help') + + def add_object(self): + """Register the widget with the platform accessibility API.""" + widget = self._widget + widget.tk.call('tk', 'accessible', 'add_acc_object', widget._w) + + def emit_selection_change(self): + """Notify the platform accessibility API that the selection + in the widget has changed.""" + widget = self._widget + widget.tk.call('tk', 'accessible', 'emit_selection_change', + widget._w) + + def emit_focus_change(self): + """Notify the platform accessibility API that the widget has + received focus. + + Not available on macOS.""" + widget = self._widget + widget.tk.call('tk', 'accessible', 'emit_focus_change', widget._w) + + class Wm: """Provides functions for the communication with the window manager.""" 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 00000000000000..d32d6a80238fdd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst @@ -0,0 +1,7 @@ +Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the +:attr:`~tkinter.Misc.accessible` property of widgets returns an +:class:`~tkinter.Accessible` object whose attributes are exposed to +assistive technologies such as screen readers and whose methods register +custom widgets with the platform accessibility API, and the +:meth:`~tkinter.Misc.tk_check_screenreader` method reports whether a +screen reader is running.