https://github.com/python/cpython/commit/c59c95f7b42844266d635e6ae60cae0091d64b56
commit: c59c95f7b42844266d635e6ae60cae0091d64b56
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-10T19:36:12+03:00
summary:

gh-143070: Use "+" instead of "!" in automatically generated tkinter widget 
names (GH-151975)

The "!" prefix has a special meaning in the tag expressions of the
canvas and text widgets ("!", "&&", "||", "^" and parentheses), so an
automatically generated widget name could not be used as a tag.  "+" has
no special meaning there, nor in option database patterns or Tcl lists,
and a user is very unlikely to start an explicit name with it.

files:
A Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst
M Lib/test/test_tkinter/test_misc.py
M Lib/test/test_tkinter/test_text.py
M Lib/test/test_tkinter/test_widgets.py
M Lib/tkinter/__init__.py

diff --git a/Lib/test/test_tkinter/test_misc.py 
b/Lib/test/test_tkinter/test_misc.py
index 92211075ce846a..9f883e6d79c201 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -49,8 +49,27 @@ class Button2(tkinter.Button):
         self.assertNotEqual(str(f), str(f2))
         b = tkinter.Button(f2)
         b2 = Button2(f2)
-        for name in str(b).split('.') + str(b2).split('.'):
+        for w in (t, f, f2, b, b2):
+            # The full path name starts with a dot, the name of the root.
+            self.assertTrue(str(w).startswith('.'), msg=repr(str(w)))
+            name = w.winfo_name()
+            # A generated name is not empty and contains no dot, which would
+            # be interpreted as a path name component separator.
+            self.assertTrue(name, msg=repr(name))
+            self.assertNotIn('.', name, msg=repr(name))
+            # A generated name can be used not only as a window name, but also
+            # as a canvas or text tag, an option database pattern or a Tcl list
+            # element, so it must avoid characters that are special there.
+            # It is marked so as not to look like a user-chosen name.
             self.assertFalse(name.isidentifier(), msg=repr(name))
+            # A capital letter starts a class name in an option pattern.
+            self.assertFalse(name[0].isupper(), msg=repr(name))
+            # "!&|^()" are operators in canvas tag expressions (gh-143070),
+            # "*" separates words in an option pattern, and whitespace and
+            # "{}[]\\"$;" are special in Tcl lists and scripts.
+            self.assertNotRegex(name, r'[][!&|^()*\s{}"\\$;]', msg=repr(name))
+            # "-", "@" and "~" are special only as the first character.
+            self.assertNotIn(name[0], '-@~', msg=repr(name))
         b3 = tkinter.Button(f2)
         b4 = Button2(f2)
         self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4)
diff --git a/Lib/test/test_tkinter/test_text.py 
b/Lib/test/test_tkinter/test_text.py
index 87cb4323fcd662..27423006a7e71b 100644
--- a/Lib/test/test_tkinter/test_text.py
+++ b/Lib/test/test_tkinter/test_text.py
@@ -389,6 +389,9 @@ def test_image(self):
         # An embedded image occupies a single index position.
         self.assertEqual(text.index('end - 1 char'), '1.3')
 
+        # The image name can be used as an index; it is matched as a whole.
+        self.assertEqual(text.index(name), '1.1')
+
         # Either a name or an image is required, and the index must be valid.
         self.assertRaises(TclError, text.image_create, '1.0')
         self.assertRaises(TclError, text.image_create, 'invalid',
@@ -405,6 +408,11 @@ def test_window(self):
                          (str(button),))
         self.assertEqual(text.window_cget('1.1', 'window'), str(button))
 
+        # The window can be addressed by its name where an index is expected;
+        # the name is matched as a whole rather than parsed (gh-143070).
+        self.assertEqual(text.index(str(button)), '1.1')
+        self.assertEqual(text.window_cget(str(button), 'window'), str(button))
+
         text.window_configure('1.1', padx=5)
         self.assertEqual(text.window_cget('1.1', 'padx'), 5)
 
diff --git a/Lib/test/test_tkinter/test_widgets.py 
b/Lib/test/test_tkinter/test_widgets.py
index 6cee18b809eef5..970eddc955f3aa 100644
--- a/Lib/test/test_tkinter/test_widgets.py
+++ b/Lib/test/test_tkinter/test_widgets.py
@@ -1486,6 +1486,12 @@ def test_find(self):
         for result in (c.find_all(), c.find_withtag(r1)):
             self.assertIsInstance(result, tuple)
 
+        # An automatically generated widget name can be used as a tag
+        # (gh-143070).
+        w = tkinter.Frame(c)
+        r4 = c.create_window(0, 0, window=w, tags=str(w))
+        self.assertEqual(c.find_withtag(str(w)), (r4,))
+
         self.assertRaises(TclError, c.find_closest, 'spam', 0)
         self.assertRaises(TclError, c.find_enclosed, 0, 0, 'spam', 0)
         self.assertRaises(TclError, c.find_overlapping, 0, 0, 'spam', 0)
diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py
index b37a504ae9ff21..d8172ff5c4087a 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -2976,16 +2976,20 @@ def _setup(self, master, cnf):
             del cnf['name']
         if not name:
             name = self.__class__.__name__.lower()
+            # Generated names are marked with a leading "+", which a user is
+            # unlikely to use, so they do not clash with explicit names.
+            # "+" is also one of the few symbols with no special meaning in
+            # canvas and text tag expressions, so the name can be used as a 
tag.
             if name[-1].isdigit():
-                name += "!"  # Avoid duplication when calculating names below
+                name += "+"  # Avoid duplication when calculating names below
             if master._last_child_ids is None:
                 master._last_child_ids = {}
             count = master._last_child_ids.get(name, 0) + 1
             master._last_child_ids[name] = count
             if count == 1:
-                name = '!%s' % (name,)
+                name = '+%s' % (name,)
             else:
-                name = '!%s%d' % (name, count)
+                name = '+%s%d' % (name, count)
         self._name = name
         if master._w=='.':
             self._w = '.' + name
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst 
b/Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst
new file mode 100644
index 00000000000000..51c2dc3d51bc86
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst
@@ -0,0 +1,3 @@
+Automatically generated :mod:`tkinter` widget names now start with ``"+"``
+instead of ``"!"``, so that they can be used as tags in the
+:class:`!tkinter.Canvas` and :class:`!tkinter.Text` widgets.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to