From 654ff70e89b4ccec149de12522deba7db4bbf3c9 Mon Sep 17 00:00:00 2001 From: Walter Doerwald Date: Mon, 8 Jun 2026 20:50:31 +0200 Subject: [PATCH 1/4] gh-151099: Better repr output for template strings. --- Lib/test/test_string/test_templatelib.py | 19 ++++++ ...-06-08-20-38-28.gh-issue-151099.6Opls1.rst | 2 + Objects/templateobject.c | 68 +++++++++++++++++-- 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst diff --git a/Lib/test/test_string/test_templatelib.py b/Lib/test/test_string/test_templatelib.py index 1c86717155fd5ab..d43fe73c453ab7c 100644 --- a/Lib/test/test_string/test_templatelib.py +++ b/Lib/test/test_string/test_templatelib.py @@ -101,6 +101,25 @@ def test_template_values(self): t = t'Hello, {name}, {age} from {country}' self.assertEqual(t.values, ("Lys", 0, "GR")) + def test_repr(self): + self.assertEqual(repr(t''), 'Template()') + self.assertEqual(repr(t'foo'), "Template('foo')") + x = 42 + self.assertEqual( + repr(t'{x}'), + "Template(Interpolation(42, 'x', None, ''))") + self.assertEqual( + repr(t'a{x!r:02}b'), + "Template('a', Interpolation(42, 'x', 'r', '02'), 'b')") + + # Test a "recursive" template + x = [] + t = t'a{x}b' + x.append(t) + self.assertEqual( + repr(t), + "Template('a', Interpolation([Template(...)], 'x', None, ''), 'b')") + def test_pickle_template(self): user = 'test' for template in ( diff --git a/Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst b/Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst new file mode 100644 index 000000000000000..6720b7fb8447ab2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst @@ -0,0 +1,2 @@ +Change :func:`repr` output of :class:`string.templatelib.Template` objects +to list the parts of the template in their original source order. diff --git a/Objects/templateobject.c b/Objects/templateobject.c index 1609e82b444516c..24aa73cbb8c211f 100644 --- a/Objects/templateobject.c +++ b/Objects/templateobject.c @@ -212,10 +212,70 @@ static PyObject * template_repr(PyObject *op) { templateobject *self = templateobject_CAST(op); - return PyUnicode_FromFormat("%s(strings=%R, interpolations=%R)", - _PyType_Name(Py_TYPE(self)), - self->strings, - self->interpolations); + + int res = Py_ReprEnter(self); + if (res != 0) { + return (res > 0 ? PyUnicode_FromString("Template(...)") : NULL); + } + + Py_ssize_t stringslen = PyTuple_GET_SIZE(self->strings); + Py_ssize_t interpolationslen = PyTuple_GET_SIZE(self->interpolations); + + PyUnicodeWriter *writer = PyUnicodeWriter_Create(10); + if (writer == NULL) { + return NULL; + } + + if (PyUnicodeWriter_WriteUTF8(writer, _PyType_Name(Py_TYPE(self)), -1) < 0) { + goto error; + } + if (PyUnicodeWriter_WriteChar(writer, '(') < 0) { + goto error; + } + + /* Render the strings and interpolations in the order they appeared in the + constructor call, i.e. interleaved and skipping empty strings. This + matches the order produced by templateiter_next. */ + int first = 1; + for (Py_ssize_t i = 0; i < stringslen; i++) { + PyObject *string = PyTuple_GET_ITEM(self->strings, i); + if (PyUnicode_GET_LENGTH(string) > 0) { + if (!first) { + if (PyUnicodeWriter_WriteASCII(writer, ", ", 2) < 0) { + goto error; + } + } + if (PyUnicodeWriter_WriteRepr(writer, string) < 0) { + goto error; + } + first = 0; + } + if (i < interpolationslen) { + PyObject *interpolation = PyTuple_GET_ITEM(self->interpolations, i); + if (!first) { + if (PyUnicodeWriter_WriteASCII(writer, ", ", 2) < 0) { + goto error; + } + } + if (PyUnicodeWriter_WriteRepr(writer, interpolation) < 0) { + goto error; + } + first = 0; + } + } + + if (PyUnicodeWriter_WriteChar(writer, ')') < 0) { + goto error; + } + + Py_ReprLeave(self); + + return PyUnicodeWriter_Finish(writer); + +error: + Py_ReprLeave(self); + PyUnicodeWriter_Discard(writer); + return NULL; } static PyObject * From 16f8ca3081e552fff79ddd0cdf52de8c2fe7c41d Mon Sep 17 00:00:00 2001 From: Walter Doerwald Date: Wed, 10 Jun 2026 09:31:34 +0200 Subject: [PATCH 2/4] Don't hardcode the class name in case of a cycle. Pass the original `op` to `Py_ReprEnter`/`Py_ReprLeave` to avoid compiler warnings. --- Objects/templateobject.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Objects/templateobject.c b/Objects/templateobject.c index 24aa73cbb8c211f..3613000a717dc8a 100644 --- a/Objects/templateobject.c +++ b/Objects/templateobject.c @@ -213,9 +213,12 @@ template_repr(PyObject *op) { templateobject *self = templateobject_CAST(op); - int res = Py_ReprEnter(self); + int res = Py_ReprEnter(op); if (res != 0) { - return (res > 0 ? PyUnicode_FromString("Template(...)") : NULL); + if (res < 0) + return NULL; + else + return PyUnicode_FromFormat("%s(...)", _PyType_Name(Py_TYPE(self))); } Py_ssize_t stringslen = PyTuple_GET_SIZE(self->strings); @@ -268,12 +271,12 @@ template_repr(PyObject *op) goto error; } - Py_ReprLeave(self); + Py_ReprLeave(op); return PyUnicodeWriter_Finish(writer); error: - Py_ReprLeave(self); + Py_ReprLeave(op); PyUnicodeWriter_Discard(writer); return NULL; } From 8ab352e2ae6619db7856294584772215f61f0674 Mon Sep 17 00:00:00 2001 From: Walter Doerwald Date: Wed, 17 Jun 2026 09:47:13 +0200 Subject: [PATCH 3/4] Update pprint implementation to mirror the new repr format for string.templatelib.Template. --- Lib/pprint.py | 15 +++----- Lib/test/test_pprint.py | 77 ++++++++++++++++++----------------------- 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 7355021998081dc..81f1089d588a820 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -737,18 +737,13 @@ def _pprint_user_string(self, object, stream, indent, allowance, context, level) def _pprint_template(self, object, stream, indent, allowance, context, level): cls_name = object.__class__.__name__ - if self._expand: - indent += self._indent_per_level - else: - indent += len(cls_name) + 1 - items = ( - ("strings", object.strings), - ("interpolations", object.interpolations), - ) + if not self._expand: + indent += len(cls_name) + stream.write(self._format_block_start(cls_name + "(", indent)) - self._format_namespace_items( - items, stream, indent, allowance, context, level + self._format_items( + object, stream, indent, allowance, context, level ) stream.write( self._format_block_end(")", indent - self._indent_per_level) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 041c2072b9e253a..94d9257a11b9fd2 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1517,41 +1517,36 @@ def test_user_string(self): def test_template(self): d = t"" - self.assertEqual(pprint.pformat(d), - "Template(strings=('',), interpolations=())") + self.assertEqual(pprint.pformat(d), "Template()") self.assertEqual(pprint.pformat(d), repr(d)) - self.assertEqual(pprint.pformat(d, width=1), -"""\ -Template(strings=('',), - interpolations=())""") + self.assertEqual(pprint.pformat(d, width=1), "Template()") name = "World" d = t"Hello {name}" self.assertEqual(pprint.pformat(d), """\ -Template(strings=('Hello ', ''), - interpolations=(Interpolation('World', 'name', None, ''),))""") +Template('Hello ', Interpolation('World', 'name', None, ''))""") ver = {3.13: False, 3.14: True} d = t"Hello { {"name": "Python", "version": ver}!s:z}!" self.assertEqual(pprint.pformat(d, width=1), """\ -Template(strings=('Hello ', - '!'), - interpolations=(Interpolation({'name': 'Python', - 'version': {3.13: False, - 3.14: True}}, - ' ' - '{"name": ' - '"Python", ' - '"version": ' - 'ver}', - 's', - 'z'),))""") +Template('Hello ', + Interpolation({'name': 'Python', + 'version': {3.13: False, + 3.14: True}}, + ' ' + '{"name": ' + '"Python", ' + '"version": ' + 'ver}', + 's', + 'z'), + '!')""") def test_expand_template(self): d = t"" self.assertEqual( pprint.pformat(d, expand=True), - "Template(strings=('',), interpolations=())", + "Template()", ) name = "World" d = t"Hello {name}" @@ -1559,14 +1554,12 @@ def test_expand_template(self): pprint.pformat(d, width=40, indent=4, expand=True), """\ Template( - strings=('Hello ', ''), - interpolations=( - Interpolation( - value='World', - expression='name', - conversion=None, - format_spec='', - ), + 'Hello ', + Interpolation( + value='World', + expression='name', + conversion=None, + format_spec='', ), )""", ) @@ -1576,22 +1569,18 @@ def test_expand_template(self): pprint.pformat(d, width=40, indent=4, expand=True), """\ Template( - strings=('Hello ', '!'), - interpolations=( - Interpolation( - value={ - 'name': 'Python', - 'version': { - 3.13: False, - 3.14: True, - }, - }, - expression=' {"name": "Python", ' - '"version": ver}', - conversion='s', - format_spec='z', - ), + 'Hello ', + Interpolation( + value={ + 'name': 'Python', + 'version': {3.13: False, 3.14: True}, + }, + expression=' {"name": "Python", ' + '"version": ver}', + conversion='s', + format_spec='z', ), + '!', )""", ) From ae95a4895381d7a644b533bf39f6c87c53f45743 Mon Sep 17 00:00:00 2001 From: Walter Doerwald Date: Thu, 25 Jun 2026 18:10:25 +0200 Subject: [PATCH 4/4] Omit conversion and format_spec in t-string repr and pprint output if they have their default value. --- Lib/pprint.py | 16 +++-- Lib/test/test_pprint.py | 86 +++++++++++++++++++++++- Lib/test/test_string/test_templatelib.py | 12 +++- Objects/interpolationobject.c | 28 +++++++- 4 files changed, 128 insertions(+), 14 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 81f1089d588a820..2a39e6b8568cab4 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -753,13 +753,15 @@ def _pprint_interpolation(self, object, stream, indent, allowance, context, leve cls_name = object.__class__.__name__ if self._expand: indent += self._indent_per_level + stream.write(self._format_block_start(cls_name + "(", indent)) items = ( ("value", object.value), ("expression", object.expression), - ("conversion", object.conversion), - ("format_spec", object.format_spec), ) - stream.write(self._format_block_start(cls_name + "(", indent)) + if object.conversion is not None or object.format_spec: + items += (("conversion", object.conversion),) + if object.format_spec: + items += (("format_spec", object.format_spec),) self._format_namespace_items( items, stream, indent, allowance, context, level ) @@ -768,13 +770,15 @@ def _pprint_interpolation(self, object, stream, indent, allowance, context, leve ) else: indent += len(cls_name) + stream.write(cls_name + "(") items = ( object.value, object.expression, - object.conversion, - object.format_spec, ) - stream.write(cls_name + "(") + if object.conversion is not None or object.format_spec: + items += (object.conversion,) + if object.format_spec: + items += (object.format_spec,) self._format_items( items, stream, indent, allowance, context, level ) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 94d9257a11b9fd2..df3eeba4ef14782 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1524,7 +1524,48 @@ def test_template(self): d = t"Hello {name}" self.assertEqual(pprint.pformat(d), """\ -Template('Hello ', Interpolation('World', 'name', None, ''))""") +Template('Hello ', Interpolation('World', 'name'))""") + d = t"Hello {name!r}" + self.assertEqual(pprint.pformat(d), +"""\ +Template('Hello ', Interpolation('World', 'name', 'r'))""") + d = t"Hello {name:0}" + self.assertEqual(pprint.pformat(d), +"""\ +Template('Hello ', Interpolation('World', 'name', None, '0'))""") + d = t"Hello {name!r:0}" + self.assertEqual(pprint.pformat(d), +"""\ +Template('Hello ', Interpolation('World', 'name', 'r', '0'))""") + d = t"Hello {name}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name'))""") + d = t"Hello {name!r}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name', + 'r'))""") + d = t"Hello {name:0}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name', + None, + '0'))""") + d = t"Hello {name!r:0}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name', + 'r', + '0'))""") ver = {3.13: False, 3.14: True} d = t"Hello { {"name": "Python", "version": ver}!s:z}!" self.assertEqual(pprint.pformat(d, width=1), @@ -1551,7 +1592,32 @@ def test_expand_template(self): name = "World" d = t"Hello {name}" self.assertEqual( - pprint.pformat(d, width=40, indent=4, expand=True), + pprint.pformat(d, width=30, indent=4, expand=True), + """\ +Template( + 'Hello ', + Interpolation( + value='World', + expression='name', + ), +)""", + ) + d = t"Hello {name!r}" + self.assertEqual( + pprint.pformat(d, width=30, indent=4, expand=True), + """\ +Template( + 'Hello ', + Interpolation( + value='World', + expression='name', + conversion='r', + ), +)""", + ) + d = t"Hello {name:0}" + self.assertEqual( + pprint.pformat(d, width=30, indent=4, expand=True), """\ Template( 'Hello ', @@ -1559,7 +1625,21 @@ def test_expand_template(self): value='World', expression='name', conversion=None, - format_spec='', + format_spec='0', + ), +)""", + ) + d = t"Hello {name!r:0}" + self.assertEqual( + pprint.pformat(d, width=30, indent=4, expand=True), + """\ +Template( + 'Hello ', + Interpolation( + value='World', + expression='name', + conversion='r', + format_spec='0', ), )""", ) diff --git a/Lib/test/test_string/test_templatelib.py b/Lib/test/test_string/test_templatelib.py index d43fe73c453ab7c..45e1ff66a884a2d 100644 --- a/Lib/test/test_string/test_templatelib.py +++ b/Lib/test/test_string/test_templatelib.py @@ -104,10 +104,18 @@ def test_template_values(self): def test_repr(self): self.assertEqual(repr(t''), 'Template()') self.assertEqual(repr(t'foo'), "Template('foo')") + + # Test various combination for present/absent conversion and format_spec x = 42 self.assertEqual( repr(t'{x}'), - "Template(Interpolation(42, 'x', None, ''))") + "Template(Interpolation(42, 'x'))") + self.assertEqual( + repr(t'{x!r}'), + "Template(Interpolation(42, 'x', 'r'))") + self.assertEqual( + repr(t'{x:02}'), + "Template(Interpolation(42, 'x', None, '02'))") self.assertEqual( repr(t'a{x!r:02}b'), "Template('a', Interpolation(42, 'x', 'r', '02'), 'b')") @@ -118,7 +126,7 @@ def test_repr(self): x.append(t) self.assertEqual( repr(t), - "Template('a', Interpolation([Template(...)], 'x', None, ''), 'b')") + "Template('a', Interpolation([Template(...)], 'x'), 'b')") def test_pickle_template(self): user = 'test' diff --git a/Objects/interpolationobject.c b/Objects/interpolationobject.c index e37724fb7852a27..9efb4f7da1333e9 100644 --- a/Objects/interpolationobject.c +++ b/Objects/interpolationobject.c @@ -112,9 +112,31 @@ static PyObject * interpolation_repr(PyObject *op) { interpolationobject *self = interpolationobject_CAST(op); - return PyUnicode_FromFormat("%s(%R, %R, %R, %R)", - _PyType_Name(Py_TYPE(self)), self->value, self->expression, - self->conversion, self->format_spec); + + /* Only emit trailing arguments that differ from their default values + (conversion=None and format_spec=""). We never use keyword arguments, so + if 'format_spec' is non-default, 'conversion' has to be emitted too even + when it still has its default value. */ + int show_format_spec = PyUnicode_GET_LENGTH(self->format_spec) > 0; + int show_conversion = show_format_spec || self->conversion != Py_None; + + if (show_format_spec) { + return PyUnicode_FromFormat("%s(%R, %R, %R, %R)", + _PyType_Name(Py_TYPE(self)), + self->value, self->expression, + self->conversion, self->format_spec); + } + else if (show_conversion) { + return PyUnicode_FromFormat("%s(%R, %R, %R)", + _PyType_Name(Py_TYPE(self)), + self->value, self->expression, + self->conversion); + } + else { + return PyUnicode_FromFormat("%s(%R, %R)", + _PyType_Name(Py_TYPE(self)), + self->value, self->expression); + } } static PyMemberDef interpolation_members[] = {