https://github.com/python/cpython/commit/8a32914f78e0a1b1238548c3d575aa8ce3854d8c
commit: 8a32914f78e0a1b1238548c3d575aa8ce3854d8c
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-10T17:29:53+03:00
summary:

gh-153513: Return str and numbers for more Tcl object types in _tkinter 
(GH-153514)

FromObj() now returns str for the "index", "window", "nsName" and
"parsedVarName" object types, whose internal representation is only a
context-bound lookup with nothing worth preserving in a Tcl_Obj.  An
"index" value is one of a small fixed set of enumeration keywords, so
its str is interned.

A "pixel" screen distance with no unit suffix is screen independent and
is now returned as an int or float.  A distance with an m/c/i/p suffix
needs a Tk_Window to be resolved and is kept as a Tcl_Obj, as are the
"dict" and the resource types "color", "border", "font" and "cursor".

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst
M Doc/library/tkinter.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_tcl.py
M Lib/test/test_tkinter/test_misc.py
M Modules/_tkinter.c

diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst
index 5ced35aadbe6d3..90bb5db7eb38d8 100644
--- a/Doc/library/tkinter.rst
+++ b/Doc/library/tkinter.rst
@@ -790,6 +790,15 @@ distance
    millimetres, ``p`` for printer's points.  For example, 3.5 inches is 
expressed
    as ``"3.5i"``.
 
+   When a screen distance option is read back (for example with :meth:`!cget`),
+   a distance with no unit suffix is returned as an :class:`int` or a 
:class:`float`.
+   A distance with a unit suffix depends on the screen resolution
+   and is returned as an opaque Tcl object that can be passed back to Tk.
+
+   .. versionchanged:: next
+      Screen distances with no unit suffix are returned as an :class:`int`
+      or a :class:`float`.
+
 font
    Tk uses a font description such as ``{courier 10 bold}``; in
    :mod:`!tkinter` this is most naturally passed as a tuple of
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 5e8abc7034bea0..9e1070e8efdaa5 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -454,6 +454,13 @@ tkinter
   :meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
   (Contributed by Serhiy Storchaka in :gh:`143990`.)
 
+* Values of several Tcl object types returned by :mod:`tkinter` are now
+  converted to the corresponding Python type instead of being wrapped in a
+  :class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and
+  ``parsedVarName`` objects to :class:`str`, and ``pixel`` screen distances
+  with no unit suffix to :class:`int` or :class:`float`.
+  (Contributed by Serhiy Storchaka in :gh:`153513`.)
+
 xml
 ---
 
diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py
index 70731d3222ced9..26be4ee0d7afe4 100644
--- a/Lib/test/test_tcl.py
+++ b/Lib/test/test_tcl.py
@@ -650,6 +650,22 @@ def testfunc(arg):
         else:
             self.assertEqual(a, expected)
 
+    def test_return_dict_object(self):
+        # A dict is returned as a Tcl_Obj to preserve its structure.
+        tcl = self.interp.tk
+        a = tcl.call('dict', 'create', 'a', 1, 'b', 2)
+        if self.wantobjects:
+            self.assertIsInstance(a, _tkinter.Tcl_Obj)
+            self.assertEqual(a.typename, 'dict')
+        self.assertEqual(str(a), 'a 1 b 2')
+
+    def test_return_nsname_object(self):
+        # An "nsName" object is returned as a str, not wrapped in a Tcl_Obj.
+        tcl = self.interp.tk
+        a = tcl.call('namespace', 'current')
+        self.assertIsInstance(a, str)
+        self.assertEqual(a, '::')
+
     def test_splitlist(self):
         splitlist = self.interp.tk.splitlist
         call = self.interp.tk.call
diff --git a/Lib/test/test_tkinter/test_misc.py 
b/Lib/test/test_tkinter/test_misc.py
index 0ebaac22a2971d..92211075ce846a 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -6,7 +6,7 @@
 import unittest
 import weakref
 import tkinter
-from tkinter import TclError
+from tkinter import TclError, ttk
 import enum
 from test import support
 from test.support import os_helper
@@ -14,7 +14,7 @@
 from test.test_tkinter.support import setUpModule  # noqa: F401
 from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
                                        requires_tk, get_tk_patchlevel,
-                                       tcl_version)
+                                       tcl_version, tk_version)
 
 support.requires('gui')
 
@@ -2054,5 +2054,65 @@ def _info_commands(widget, pattern=None):
     return widget.tk.splitlist(widget.tk.call('info', 'commands', pattern))
 
 
+class TclObjTypeTest(AbstractTkTest, unittest.TestCase):
+    # See FromObj() in Modules/_tkinter.c: Tcl object types are converted to
+    # appropriate Python types.  These conversions only happen in the object
+    # mode, so skip when it is disabled.
+
+    def setUp(self):
+        super().setUp()
+        if not self.wantobjects:
+            self.skipTest('requires wantobjects')
+
+    def test_enum_option_returns_str(self):
+        # An "index" object (an enumeration keyword) is returned as a str.
+        w = ttk.Scale(self.root, orient='horizontal')
+        value = w.cget('orient')
+        self.assertIsInstance(value, str)
+        self.assertEqual(value, 'horizontal')
+
+    def test_enum_option_is_interned(self):
+        # Equal "index" keywords share a single interned str object.
+        a = ttk.Scale(self.root, orient='horizontal').cget('orient')
+        b = ttk.Scale(self.root, orient='horizontal').cget('orient')
+        self.assertIs(a, b)
+
+    def test_window_option_returns_str(self):
+        # A "window" object is returned as a str.
+        label = tkinter.Label(self.root)
+        w = ttk.LabelFrame(self.root, labelwidget=label)
+        value = w.cget('labelwidget')
+        self.assertIsInstance(value, str)
+        self.assertEqual(value, str(label))
+
+    def test_variable_option_returns_str(self):
+        # A "parsedVarName" object is returned as a str.
+        w = tkinter.Checkbutton(self.root)
+        self.assertIsInstance(w.cget('variable'), str)
+
+    def test_pixel_option_without_unit_returns_number(self):
+        # A screen distance with no unit suffix is already in pixels and thus
+        # screen independent, so it is returned as an int or a float.
+        w = tkinter.Frame(self.root)
+        w['borderwidth'] = 3
+        self.assertIsInstance(w.cget('borderwidth'), int)
+        self.assertEqual(w.cget('borderwidth'), 3)
+        if tk_version >= (9, 0):
+            # Tk < 9 rounds a fractional screen distance to an integer.
+            w['borderwidth'] = 2.5
+            self.assertIsInstance(w.cget('borderwidth'), float)
+            self.assertEqual(w.cget('borderwidth'), 2.5)
+
+    @requires_tk(9, 0)
+    def test_pixel_option_with_unit_is_not_a_number(self):
+        # A screen distance with an m/c/i/p suffix depends on the screen
+        # resolution, so it is not converted to a number here.  (Tk < 9 
resolves
+        # it eagerly to an integer pixel count when the option is read.)
+        w = tkinter.Frame(self.root)
+        w['borderwidth'] = '3m'
+        self.assertNotIsInstance(w.cget('borderwidth'), (int, float))
+        self.assertEqual(str(w.cget('borderwidth')), '3m')
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst 
b/Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst
new file mode 100644
index 00000000000000..a374355d2d4c59
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst
@@ -0,0 +1,5 @@
+Values of several Tcl object types returned by :mod:`tkinter` are now
+converted to the corresponding Python type instead of being wrapped in a
+:class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and
+``parsedVarName`` objects to :class:`str`, and ``pixel`` screen distances
+with no unit suffix to :class:`int` or :class:`float`.
diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c
index 62a79128d9726c..30185c08eeabc9 100644
--- a/Modules/_tkinter.c
+++ b/Modules/_tkinter.c
@@ -356,6 +356,15 @@ typedef struct {
     const Tcl_ObjType *StringType;
     const Tcl_ObjType *UTF32StringType;
     const Tcl_ObjType *PixelType;
+    const Tcl_ObjType *IndexType;
+    const Tcl_ObjType *ColorType;
+    const Tcl_ObjType *BorderType;
+    const Tcl_ObjType *FontType;
+    const Tcl_ObjType *CursorType;
+    const Tcl_ObjType *WindowType;
+    const Tcl_ObjType *NsNameType;
+    const Tcl_ObjType *ParsedVarNameType;
+    const Tcl_ObjType *DictType;
 } TkappObject;
 
 #define TkappObject_CAST(op)    ((TkappObject *)(op))
@@ -687,11 +696,22 @@ Tkapp_New(const char *screenName, const char *className,
         Tcl_DecrRefCount(value);
     }
     v->WideIntType = Tcl_GetObjType("wideInt");
-    v->BignumType = Tcl_GetObjType("bignum");
     v->ListType = Tcl_GetObjType("list");
     v->StringType = Tcl_GetObjType("string");
     v->UTF32StringType = Tcl_GetObjType("utf32string");
+    /* These types may not be registered until first used, so these may be 
NULL;
+       FromObj() caches them lazily on first encounter. */
+    v->BignumType = Tcl_GetObjType("bignum");
     v->PixelType = Tcl_GetObjType("pixel");
+    v->IndexType = Tcl_GetObjType("index");
+    v->ColorType = Tcl_GetObjType("color");
+    v->BorderType = Tcl_GetObjType("border");
+    v->FontType = Tcl_GetObjType("font");
+    v->CursorType = Tcl_GetObjType("cursor");
+    v->WindowType = Tcl_GetObjType("window");
+    v->NsNameType = Tcl_GetObjType("nsName");
+    v->ParsedVarNameType = Tcl_GetObjType("parsedVarName");
+    v->DictType = Tcl_GetObjType("dict");
 
     /* Delete the 'exit' command, which can screw things up */
     Tcl_DeleteCommand(v->interp, "exit");
@@ -1257,6 +1277,39 @@ fromBignumObj(TkappObject *tkapp, Tcl_Obj *value)
     return res;
 }
 
+/* Convert a "pixel" screen distance to a Python object.  A value with no unit
+   suffix is screen independent and is returned as an int or float.  A value 
with
+   an m/c/i/p suffix needs a Tk_Window to be resolved to pixels, which is not
+   available here, so it is kept as a Tcl_Obj.  Tcl_Get*FromObj() only set the
+   object's type on success, so a unit-bearing value does not shimmer. */
+static PyObject*
+fromPixelObj(TkappObject *tkapp, Tcl_Obj *value)
+{
+    PyObject *result = fromWideIntObj(tkapp, value);
+    if (result != NULL || PyErr_Occurred()) {
+        return result;
+    }
+    Tcl_ResetResult(Tkapp_Interp(tkapp));
+    double doubleValue;
+    if (Tcl_GetDoubleFromObj(NULL, value, &doubleValue) == TCL_OK) {
+        return PyFloat_FromDouble(doubleValue);
+    }
+    return newPyTclObject(value);
+}
+
+/* Convert an "index" object to a Python str.  Index values are enumeration
+   keywords from a small fixed set (e.g. "horizontal", "disabled"), so the 
result
+   is interned to share one str object per keyword. */
+static PyObject*
+fromIndexObj(TkappObject *tkapp, Tcl_Obj *value)
+{
+    PyObject *result = unicodeFromTclObj(tkapp, value);
+    if (result != NULL) {
+        PyUnicode_InternInPlace(&result);
+    }
+    return result;
+}
+
 static PyObject*
 FromObj(TkappObject *tkapp, Tcl_Obj *value)
 {
@@ -1327,19 +1380,96 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value)
     }
 
     if (value->typePtr == tkapp->StringType ||
-        value->typePtr == tkapp->UTF32StringType ||
-        value->typePtr == tkapp->PixelType)
+        value->typePtr == tkapp->UTF32StringType)
     {
         return unicodeFromTclObj(tkapp, value);
     }
 
-    if (tkapp->BignumType == NULL &&
-        strcmp(value->typePtr->name, "bignum") == 0) {
-        /* bignum type is not registered in Tcl */
+    if (value->typePtr == tkapp->PixelType) {
+        return fromPixelObj(tkapp, value);
+    }
+
+    if (value->typePtr == tkapp->IndexType) {
+        return fromIndexObj(tkapp, value);
+    }
+
+    /* A dict is kept as a Tcl_Obj to preserve its structure for _splitdict(). 
*/
+    if (value->typePtr == tkapp->DictType) {
+        return newPyTclObject(value);
+    }
+
+    /* Resource types are kept as a Tcl_Obj so that Tk can reuse the parsed
+       X/font resource when the object is passed back to it. */
+    if (value->typePtr == tkapp->ColorType ||
+        value->typePtr == tkapp->BorderType ||
+        value->typePtr == tkapp->FontType ||
+        value->typePtr == tkapp->CursorType) {
+        return newPyTclObject(value);
+    }
+
+    /* These types are context-bound lookups (a cached window or namespace
+       resolution, or a parsed variable name) with nothing worth preserving in
+       a Tcl_Obj, so they are returned as str. */
+    if (value->typePtr == tkapp->WindowType ||
+        value->typePtr == tkapp->NsNameType ||
+        value->typePtr == tkapp->ParsedVarNameType) {
+        return unicodeFromTclObj(tkapp, value);
+    }
+
+    /* The types below may not be registered until first used, so they are
+       matched by name and cached on first encounter; the pointer checks above
+       then catch them. */
+    const char *name = value->typePtr->name;
+
+    if (tkapp->BignumType == NULL && strcmp(name, "bignum") == 0) {
         tkapp->BignumType = value->typePtr;
         return fromBignumObj(tkapp, value);
     }
 
+    if (tkapp->DictType == NULL && strcmp(name, "dict") == 0) {
+        tkapp->DictType = value->typePtr;
+        return newPyTclObject(value);
+    }
+
+    if (tkapp->PixelType == NULL && strcmp(name, "pixel") == 0) {
+        tkapp->PixelType = value->typePtr;
+        return fromPixelObj(tkapp, value);
+    }
+
+    if (tkapp->IndexType == NULL && strcmp(name, "index") == 0) {
+        tkapp->IndexType = value->typePtr;
+        return fromIndexObj(tkapp, value);
+    }
+
+    if (tkapp->WindowType == NULL && strcmp(name, "window") == 0) {
+        tkapp->WindowType = value->typePtr;
+        return unicodeFromTclObj(tkapp, value);
+    }
+
+    if (tkapp->NsNameType == NULL && strcmp(name, "nsName") == 0) {
+        tkapp->NsNameType = value->typePtr;
+        return unicodeFromTclObj(tkapp, value);
+    }
+
+    if (tkapp->ParsedVarNameType == NULL && strcmp(name, "parsedVarName") == 
0) {
+        tkapp->ParsedVarNameType = value->typePtr;
+        return unicodeFromTclObj(tkapp, value);
+    }
+
+    /* Any other type is returned as a Tcl_Obj.  Cache the resource types so 
the
+       pointer check above catches them next time. */
+    if (tkapp->ColorType == NULL && strcmp(name, "color") == 0) {
+        tkapp->ColorType = value->typePtr;
+    }
+    else if (tkapp->BorderType == NULL && strcmp(name, "border") == 0) {
+        tkapp->BorderType = value->typePtr;
+    }
+    else if (tkapp->FontType == NULL && strcmp(name, "font") == 0) {
+        tkapp->FontType = value->typePtr;
+    }
+    else if (tkapp->CursorType == NULL && strcmp(name, "cursor") == 0) {
+        tkapp->CursorType = value->typePtr;
+    }
     return newPyTclObject(value);
 }
 

_______________________________________________
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