From fd0bd350db0ebbedd9325ab5dce99834b5f3ec69 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 7 Jul 2026 10:04:22 +0300 Subject: [PATCH 1/3] 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 653a91c5cbedba1..00384e77b5f2268 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -2023,6 +2023,31 @@ Base and mixin classes .. versionadded:: 3.16 + 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. @@ -2455,6 +2480,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 4af5d3c0bd072e2..cd4ab24193ac47b 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -812,6 +812,15 @@ 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 + :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 59769b7250d76ac..9935d6febc45b08 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -712,6 +712,42 @@ def test_tk_print(self): self.assertNotHasAttr(tkinter.Frame, 'tk_print') self.assertNotHasAttr(tkinter.Misc, 'tk_print') + @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 f06807a6cb9c7d7..ceb591091cd86b3 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -821,6 +821,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. @@ -2310,6 +2329,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 000000000000000..d32d6a80238fdd3 --- /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. From 8a6805c9e2d31a9419389e855221a0022f7edcc9 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 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 9935d6febc45b08..63adbdbafbd7235 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -728,7 +728,12 @@ def test_accessible(self): acc = f.accessible self.assertIsInstance(acc, tkinter.Accessible) self.assertIs(f.accessible, acc) + # The role has to be set first, it creates the table in which + # other attributes are stored. + acc.role = 'Label' + result = acc.role if sr: + self.assertEqual(result, 'Label') # Reading an attribute that has not been set raises TclError. with self.assertRaises(tkinter.TclError): acc.help @@ -739,10 +744,6 @@ def test_accessible(self): 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': From 91a4c57909d0322245c0a75d8d873018d2f9b54f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 6 Nov 2026 14:53:00 +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 00384e77b5f2268..c4a7ddc13b64937 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -2491,6 +2491,10 @@ Base and mixin classes Setting an attribute overrides what Tk exposes for the widget; reading an attribute that has not been set raises :exc:`~tkinter.TclError`. + 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, set :attr:`!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 ceb591091cd86b3..8147b2898139710 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -2353,6 +2353,7 @@ class Accessible: 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. + A widget must have a role before other attributes can be used. """ def __init__(self, widget):