From 5a8dbfac1c7511fa4026ffeb67a04393b2bd9729 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 6 Jun 2025 15:47:34 -0700 Subject: [PATCH 01/25] Initial commit. --- Doc/library/string.rst | 6 ++ Doc/library/string.templatelib.rst | 109 +++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 Doc/library/string.templatelib.rst diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 23e15780075435..6987d5653fdacc 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -972,3 +972,9 @@ Helper functions or ``None``, runs of whitespace characters are replaced by a single space and leading and trailing whitespace are removed, otherwise *sep* is used to split and join the words. + + + +.. toctree:: + + string.templatelib.rst diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst new file mode 100644 index 00000000000000..b052084e5e23db --- /dev/null +++ b/Doc/library/string.templatelib.rst @@ -0,0 +1,109 @@ +:mod:`!string.templatelib` --- Templates and Interpolations for t-strings +========================================================================= + +.. module:: string.templatelib + :synopsis: Support for t-string literals. + +**Source code:** :source:`Lib/string/templatelib.py` + +-------------- + + +.. seealso:: + + :ref:`f-strings` -- Format strings (f-strings) + + +.. _templatelib-template: + +Template +-------- + +The :class:`Template` class describes the contents of a template string. + +The most common way to create a new :class:`Template` instance is to use the t-string literal syntax. This syntax is identical to that of :ref:`f-strings`, except that the string is prefixed with a ``t`` instead of an ``f``. For example, the following code creates a :class:`Template` that can be used to format strings: + + >>> name = "World" + >>> greeting = t"Hello {name}!" + >>> print(list(greeting)) + ['Hello ', Interpolation('World'), '!'] + +It is also possible to create a :class:`Template` directly, using its constructor. This takes an arbitrary collection of strings and :class:`Interpolation` instances: + + >>> from string.templatelib import Template, Interpolation + >>> name = "World" + >>> greeting = Template("Hello, ", Interpolation(name), "!") + >>> print(list(greeting)) + ['Hello, ', Interpolation('World'), '!'] + +.. class:: Template(*args) + + Create a new :class:`Template` object. + + :param args: A mix of strings and :class:`Interpolation` instances in any order. + :type args: str | Interpolation + + If two or more consecutive strings are passed, they will be concatenated into a single value in the :attr:`~Template.strings` attribute. For example, the following code creates a :class:`Template` with a single final string: + + >>> from string.templatelib import Template + >>> greeting = Template("Hello ", "World", "!") + >>> print(greeting.strings) + ('Hello World!',) + + If two or more consecutive interpolations are passed, they will be treated as separate interpolations and an empty string will be inserted between them. For example, the following code creates a template with a single value in the :attr:`~Template.strings` attribute: + + >>> from string.templatelib import Template, Interpolation + >>> greeting = Template(Interpolation("World"), Interpolation("!")) + >>> print(greeting.strings) + ('',) + + .. attribute:: strings + + A :ref:`tuple ` of the static strings in the template. + + >>> name = "World" + >>> print(t"Hello {name}!".strings) + ('Hello ', '!') + + Empty strings *are* included in the tuple: + + >>> name = "World" + >>> print(t"Hello {name}{name}!".strings) + ('Hello ', '', '!') + + .. attribute:: interpolations: tuple[Interpolation, ...] + + A tuple of the interpolations in the template. + + >>> name = "World" + >>> print(t"Hello {name}!".interpolations) + (Interpolation('World'),) + + + .. attribute:: values: tuple[Any, ...] + + A tuple of all interpolated values in the template. + + >>> name = "World" + >>> print(t"Hello {name}!".values) + ('World',) + + .. method:: __iter__() -> typing.Iterator[str | Interpolation] + + Iterate over the template, yielding each string and :class:`Interpolation` in order. + + >>> name = "World" + >>> print(list(t"Hello {name}!")) + ['Hello ', Interpolation('World'), '!'] + + Empty strings are *not* included in the iteration: + + >>> name = "World" + >>> print(list(t"Hello {name}{name}")) + ['Hello ', Interpolation('World'), Interpolation('World')] + + + + + + From 09a1e9ebc9088c3262d0f5d923fb8585c2e37ec4 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 10:49:07 -0400 Subject: [PATCH 02/25] Make clear the preceding discussion about t-strings creating an instance of `templatelib.Template`. --- Doc/library/string.templatelib.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index b052084e5e23db..de8c156e6ed947 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -25,6 +25,8 @@ The most common way to create a new :class:`Template` instance is to use the t-s >>> name = "World" >>> greeting = t"Hello {name}!" + >>> type(greeting) + >>> print(list(greeting)) ['Hello ', Interpolation('World'), '!'] From ec44c2b4df3a09c1dbd2f4e08b08dffcb0ae8e09 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 11:15:44 -0400 Subject: [PATCH 03/25] Get the import order sorted. --- Doc/library/string.templatelib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index de8c156e6ed947..fabb71c59bb761 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -32,7 +32,7 @@ The most common way to create a new :class:`Template` instance is to use the t-s It is also possible to create a :class:`Template` directly, using its constructor. This takes an arbitrary collection of strings and :class:`Interpolation` instances: - >>> from string.templatelib import Template, Interpolation + >>> from string.templatelib import Interpolation, Template >>> name = "World" >>> greeting = Template("Hello, ", Interpolation(name), "!") >>> print(list(greeting)) @@ -54,7 +54,7 @@ It is also possible to create a :class:`Template` directly, using its constructo If two or more consecutive interpolations are passed, they will be treated as separate interpolations and an empty string will be inserted between them. For example, the following code creates a template with a single value in the :attr:`~Template.strings` attribute: - >>> from string.templatelib import Template, Interpolation + >>> from string.templatelib import Interpolation, Template >>> greeting = Template(Interpolation("World"), Interpolation("!")) >>> print(greeting.strings) ('',) From d8904b671d4c12e6f1e1e861071633d3134b5dcf Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 11:24:15 -0400 Subject: [PATCH 04/25] Correct the Interpolation() calls. --- Doc/library/string.templatelib.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index fabb71c59bb761..34128dd823c8ff 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -34,7 +34,7 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> from string.templatelib import Interpolation, Template >>> name = "World" - >>> greeting = Template("Hello, ", Interpolation(name), "!") + >>> greeting = Template("Hello, ", Interpolation(name, "name"), "!") >>> print(list(greeting)) ['Hello, ', Interpolation('World'), '!'] @@ -55,9 +55,9 @@ It is also possible to create a :class:`Template` directly, using its constructo If two or more consecutive interpolations are passed, they will be treated as separate interpolations and an empty string will be inserted between them. For example, the following code creates a template with a single value in the :attr:`~Template.strings` attribute: >>> from string.templatelib import Interpolation, Template - >>> greeting = Template(Interpolation("World"), Interpolation("!")) + >>> greeting = Template(Interpolation("World", "name"), Interpolation("!", "punctuation")) >>> print(greeting.strings) - ('',) + ('', '', '') .. attribute:: strings From 550aa6d4bef57fbc4e312692ea44c1ba2e218b86 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 12:41:40 -0400 Subject: [PATCH 05/25] Add documentation for Interpolation. --- Doc/library/string.templatelib.rst | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index 34128dd823c8ff..fc62fdd4a66bb3 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -104,8 +104,46 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> print(list(t"Hello {name}{name}")) ['Hello ', Interpolation('World'), Interpolation('World')] +.. class:: Interpolation(*args) + Create a new :class:`Interpolation` object. + :param value: The evaluated, in-scope result of the interpolation. + :type value: object + :param expression: The original *text* of the interpolation's Python :ref:`expressions `. + :type expression: str + :param conversion: The optional :ref:`conversion ` to be used, one of r, s, and a,. + :type value: Literal["a", "r", "s"] | None + :param format_spec: An optional, arbitrary string used as the :ref:`format specification ` to present the value. + :type expression: str = "" + + The :class:`Interpolation` type represents an expression inside a template string. It is shallow immutable -- its attributes cannot be reassigned. + + >>> name = "World" + >>> template = t"Hello {name}" + >>> template.interpolations[0].value + 'World' + >>> template.interpolations[0].value = "Galaxy" + Traceback (most recent call last): + File "", line 1, in + AttributeError: readonly attribute + + While f-strings and t-strings are largely similar in syntax and expectations, the :attr:`~Interpolation.conversion` and :attr:`~Interpolation.format_spec` behave differently. With f-strings, these are applied to the resulting value automatically. For example, in this ``format_spec``: + + >>> value = 42 + >>> f"Value: {value:.2f}" + 'Value: 42.00' + + With a t-string :class:`!Interpolation`, the template function is expected to apply this to the value: + + >>> value = 42 + >>> template = t"Value: {value:.2f}" + >> template.interpolations[0].value + 42 + + .. property:: __match_args__: (Literal["value"], Literal["expression"], Literal["conversion"], Literal["format_spec"]) + + The allowed positional arguments used by destructuring during structural pattern matching. From a20e058b9433a1cb6374d4bbd235017c6c434ae8 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 12:56:27 -0400 Subject: [PATCH 06/25] Convert to use Sphinx modifiers for type, returns, and rtype. --- Doc/library/string.templatelib.rst | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index fc62fdd4a66bb3..a99fd813e396b4 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -60,6 +60,7 @@ It is also possible to create a :class:`Template` directly, using its constructo ('', '', '') .. attribute:: strings + :type: tuple[str, ...] A :ref:`tuple ` of the static strings in the template. @@ -73,7 +74,8 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> print(t"Hello {name}{name}!".strings) ('Hello ', '', '!') - .. attribute:: interpolations: tuple[Interpolation, ...] + .. attribute:: interpolations + :type: tuple[Interpolation, ...] A tuple of the interpolations in the template. @@ -82,7 +84,8 @@ It is also possible to create a :class:`Template` directly, using its constructo (Interpolation('World'),) - .. attribute:: values: tuple[Any, ...] + .. attribute:: values + :type: tuple[Any, ...] A tuple of all interpolated values in the template. @@ -90,7 +93,7 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> print(t"Hello {name}!".values) ('World',) - .. method:: __iter__() -> typing.Iterator[str | Interpolation] + .. method:: __iter__() Iterate over the template, yielding each string and :class:`Interpolation` in order. @@ -104,6 +107,9 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> print(list(t"Hello {name}{name}")) ['Hello ', Interpolation('World'), Interpolation('World')] + :returns: An iterable of all the parts in the template. + :rtype: typing.Iterator[str | Interpolation] + .. class:: Interpolation(*args) Create a new :class:`Interpolation` object. @@ -144,6 +150,7 @@ It is also possible to create a :class:`Template` directly, using its constructo >> template.interpolations[0].value 42 - .. property:: __match_args__: (Literal["value"], Literal["expression"], Literal["conversion"], Literal["format_spec"]) + .. property:: __match_args__ - The allowed positional arguments used by destructuring during structural pattern matching. + :returns: A tuple of the attributes to use for structural pattern matching. + :rtype: (Literal["value"], Literal["expression"], Literal["conversion"], Literal["format_spec"]) From 1f30739db47da95e651fa6d901ca58b984ae1cb5 Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 15:09:52 -0400 Subject: [PATCH 07/25] Add an entry to the glossary. --- Doc/glossary.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/glossary.rst b/Doc/glossary.rst index c5c7994f1262a9..fad892998480d3 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -1304,6 +1304,11 @@ Glossary See also :term:`borrowed reference`. + t-string + String literals prefixed with ``'t'`` or ``'T'`` are commonly called + "t-strings" which is short for + :ref:`template string literals `. See also :pep:`750`. + text encoding A string in Python is a sequence of Unicode code points (in range ``U+0000``--``U+10FFFF``). To store or transfer a string, it needs to be From d935dd6fff0976f96e540f528cc6ab6d60b4f4bc Mon Sep 17 00:00:00 2001 From: pauleveritt Date: Sun, 8 Jun 2025 16:54:22 -0400 Subject: [PATCH 08/25] Find any occurrences of f-strings that should also mention t-strings. --- Doc/library/string.rst | 7 ++++--- Doc/reference/compound_stmts.rst | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 6987d5653fdacc..614b714c2baf52 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -198,8 +198,9 @@ Format String Syntax The :meth:`str.format` method and the :class:`Formatter` class share the same syntax for format strings (although in the case of :class:`Formatter`, subclasses can define their own format string syntax). The syntax is -related to that of :ref:`formatted string literals `, but it is -less sophisticated and, in particular, does not support arbitrary expressions. +related to that of :ref:`formatted string literals ` and +:ref:`template string literals `, but it is less sophisticated +and, in particular, does not support arbitrary expressions. .. index:: single: {} (curly brackets); in string formatting @@ -306,7 +307,7 @@ Format Specification Mini-Language "Format specifications" are used within replacement fields contained within a format string to define how individual values are presented (see -:ref:`formatstrings` and :ref:`f-strings`). +:ref:`formatstrings`, :ref:`f-strings`, and :ref:`t-strings`). They can also be passed directly to the built-in :func:`format` function. Each formattable type may define how the format specification is to be interpreted. diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index e95fa3a6424e23..a416cbb4cc8eab 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -852,8 +852,8 @@ A literal pattern corresponds to most The rule ``strings`` and the token ``NUMBER`` are defined in the :doc:`standard Python grammar <./grammar>`. Triple-quoted strings are -supported. Raw strings and byte strings are supported. :ref:`f-strings` are -not supported. +supported. Raw strings and byte strings are supported. :ref:`f-strings` +and :ref:`t-strings` are not supported. The forms ``signed_number '+' NUMBER`` and ``signed_number '-' NUMBER`` are for expressing :ref:`complex numbers `; they require a real number From 1e47362b849a51955ff7dcf468dfd6e9953d783f Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 12 Jun 2025 17:31:13 +0000 Subject: [PATCH 09/25] Fix doctests for str.templatelib --- Doc/library/string.templatelib.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index a99fd813e396b4..523a4b721a451e 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -28,7 +28,7 @@ The most common way to create a new :class:`Template` instance is to use the t-s >>> type(greeting) >>> print(list(greeting)) - ['Hello ', Interpolation('World'), '!'] + ['Hello ', Interpolation('World', 'name', None, ''), '!'] It is also possible to create a :class:`Template` directly, using its constructor. This takes an arbitrary collection of strings and :class:`Interpolation` instances: @@ -36,7 +36,7 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> name = "World" >>> greeting = Template("Hello, ", Interpolation(name, "name"), "!") >>> print(list(greeting)) - ['Hello, ', Interpolation('World'), '!'] + ['Hello, ', Interpolation('World', 'name', None, ''), '!'] .. class:: Template(*args) @@ -81,7 +81,7 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> name = "World" >>> print(t"Hello {name}!".interpolations) - (Interpolation('World'),) + (Interpolation('World', 'name', None, ''),) .. attribute:: values @@ -99,13 +99,13 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> name = "World" >>> print(list(t"Hello {name}!")) - ['Hello ', Interpolation('World'), '!'] + ['Hello ', Interpolation('World', 'name', None, ''), '!'] Empty strings are *not* included in the iteration: >>> name = "World" >>> print(list(t"Hello {name}{name}")) - ['Hello ', Interpolation('World'), Interpolation('World')] + ['Hello ', Interpolation('World', 'name', None, ''), Interpolation('World', 'name', None, '')] :returns: An iterable of all the parts in the template. :rtype: typing.Iterator[str | Interpolation] @@ -147,7 +147,7 @@ It is also possible to create a :class:`Template` directly, using its constructo >>> value = 42 >>> template = t"Value: {value:.2f}" - >> template.interpolations[0].value + >>> template.interpolations[0].value 42 .. property:: __match_args__ From 7b660be31cc6ced394e8ce3d6437ae1aab11f9a6 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 12 Jun 2025 17:59:33 +0000 Subject: [PATCH 10/25] Start writing the t-strings part of lexical analysis --- Doc/reference/lexical_analysis.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index b22eb4db7945d1..4af02d43f083e3 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -913,6 +913,31 @@ See also :pep:`498` for the proposal that added formatted string literals, and :meth:`str.format`, which uses a related format string mechanism. +.. _t-strings: +.. _template-string-literals: + +t-strings +--------- + +.. versionadded:: 3.14 + +A :dfn:`template string literal` or :dfn:`t-string` is a string literal +that is prefixed with ``'t'`` or ``'T'``. These strings follow the same +syntax and evaluation rules as `formatted string literals `_, with +the following differences: + +- Rather than evaluating to a `str` object, t-strings evaluate to a + `Template` object from the :mod:`string.templatelib` module. + +- Evaluated expressions are *not* formatted using the + :func:`format` protocol; :meth:`~object.__format__` is *not* invoked. Instead, + the expressions are evaluated and a new `Interpolation` object (also from the + :mod:`string.templatelib` module) is created, which contains the evaluated + value of the expression. That `Interpolation` object is found in the containing + `Template`. + + + .. _numbers: Numeric literals From fcd74e64c74492873d42533d45fc5200919a7a80 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 17 Jun 2025 17:22:47 +0000 Subject: [PATCH 11/25] Fix lint issues --- Doc/library/string.templatelib.rst | 12 ++++++------ Doc/reference/lexical_analysis.rst | 13 +++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index 523a4b721a451e..63e27fcf1e6297 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -19,9 +19,9 @@ Template -------- -The :class:`Template` class describes the contents of a template string. +The :class:`!Template` class describes the contents of a template string. -The most common way to create a new :class:`Template` instance is to use the t-string literal syntax. This syntax is identical to that of :ref:`f-strings`, except that the string is prefixed with a ``t`` instead of an ``f``. For example, the following code creates a :class:`Template` that can be used to format strings: +The most common way to create a new :class:`!Template` instance is to use the t-string literal syntax. This syntax is identical to that of :ref:`f-strings`, except that the string is prefixed with a ``t`` instead of an ``f``. For example, the following code creates a :class:`Template` that can be used to format strings: >>> name = "World" >>> greeting = t"Hello {name}!" @@ -30,7 +30,7 @@ The most common way to create a new :class:`Template` instance is to use the t-s >>> print(list(greeting)) ['Hello ', Interpolation('World', 'name', None, ''), '!'] -It is also possible to create a :class:`Template` directly, using its constructor. This takes an arbitrary collection of strings and :class:`Interpolation` instances: +It is also possible to create a :class:`!Template` directly, using its constructor. This takes an arbitrary collection of strings and :class:`Interpolation` instances: >>> from string.templatelib import Interpolation, Template >>> name = "World" @@ -40,7 +40,7 @@ It is also possible to create a :class:`Template` directly, using its constructo .. class:: Template(*args) - Create a new :class:`Template` object. + Create a new :class:`!Template` object. :param args: A mix of strings and :class:`Interpolation` instances in any order. :type args: str | Interpolation @@ -112,7 +112,7 @@ It is also possible to create a :class:`Template` directly, using its constructo .. class:: Interpolation(*args) - Create a new :class:`Interpolation` object. + Create a new :class:`!Interpolation` object. :param value: The evaluated, in-scope result of the interpolation. :type value: object @@ -126,7 +126,7 @@ It is also possible to create a :class:`Template` directly, using its constructo :param format_spec: An optional, arbitrary string used as the :ref:`format specification ` to present the value. :type expression: str = "" - The :class:`Interpolation` type represents an expression inside a template string. It is shallow immutable -- its attributes cannot be reassigned. + The :class:`!Interpolation` type represents an expression inside a template string. It is shallow immutable -- its attributes cannot be reassigned. >>> name = "World" >>> template = t"Hello {name}" diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index 4af02d43f083e3..f2a8cf9e6afd37 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -926,15 +926,16 @@ that is prefixed with ``'t'`` or ``'T'``. These strings follow the same syntax and evaluation rules as `formatted string literals `_, with the following differences: -- Rather than evaluating to a `str` object, t-strings evaluate to a - `Template` object from the :mod:`string.templatelib` module. +- Rather than evaluating to a ``str`` object, t-strings evaluate to a + :class:`~string.templatelib.Template` object from the + :mod:`string.templatelib` module. - Evaluated expressions are *not* formatted using the :func:`format` protocol; :meth:`~object.__format__` is *not* invoked. Instead, - the expressions are evaluated and a new `Interpolation` object (also from the - :mod:`string.templatelib` module) is created, which contains the evaluated - value of the expression. That `Interpolation` object is found in the containing - `Template`. + the expressions are evaluated and a new :class:`~string.templatelib.Interpolation`` + object (also from the :mod:`string.templatelib` module) is created, which + contains the evaluated value of the expression. That ``Interpolation`` object + is found in the containing ``Template``. From a796f5d3ec34196af677c9431f7b05094e93fe6b Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 17 Jun 2025 17:40:05 +0000 Subject: [PATCH 12/25] Fix further sphinx warnings --- Doc/library/string.templatelib.rst | 32 ++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index 63e27fcf1e6297..8b9fed96008e71 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -11,7 +11,7 @@ .. seealso:: - :ref:`f-strings` -- Format strings (f-strings) + :ref:`Format strings ` .. _templatelib-template: @@ -124,7 +124,6 @@ It is also possible to create a :class:`!Template` directly, using its construct :type value: Literal["a", "r", "s"] | None :param format_spec: An optional, arbitrary string used as the :ref:`format specification ` to present the value. - :type expression: str = "" The :class:`!Interpolation` type represents an expression inside a template string. It is shallow immutable -- its attributes cannot be reassigned. @@ -154,3 +153,32 @@ It is also possible to create a :class:`!Template` directly, using its construct :returns: A tuple of the attributes to use for structural pattern matching. :rtype: (Literal["value"], Literal["expression"], Literal["conversion"], Literal["format_spec"]) + + + .. property:: value + + :returns: The evaluated value of the interpolation. + :rtype: object + + .. property:: expression + + :returns: The original text of the interpolation's Python expression if the interpolation was created from a t-string literal + :rtype: str + + The :attr:`~Interpolation.expression` is the original text of the interpolation's Python expression, if the interpolation was created from a t-string literal. Developers creating + interpolations manually should either set this to an empty + string or choose a suitable valid python expression. + + .. property:: conversion + + :returns: The conversion to apply to the value, one of "a", "r", or "s", or None. + :rtype: Literal["a", "r", "s"] | None + + The :attr:`~Interpolation.conversion` is the optional conversion to apply to the value. This is one of "a", "r", or "s", or None if no conversion is specified. + + .. property:: format_spec + + :returns: The format specification to apply to the value. + :rtype: str + + The :attr:`~Interpolation.format_spec` is an optional, arbitrary string used as the format specification to present the value. This is similar to the format specification used in :ref:`format strings `, but it is not limited to a specific set of formats. From 21d337c7223aaafd4d2b96e5f59e4ada2f4e579d Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 17 Jun 2025 18:02:12 +0000 Subject: [PATCH 13/25] More! --- Doc/reference/lexical_analysis.rst | 13 ++++++------- Doc/sphinx-warnings.txt | 0 2 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 Doc/sphinx-warnings.txt diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index f2a8cf9e6afd37..a6936b1d36a6de 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -923,20 +923,19 @@ t-strings A :dfn:`template string literal` or :dfn:`t-string` is a string literal that is prefixed with ``'t'`` or ``'T'``. These strings follow the same -syntax and evaluation rules as `formatted string literals `_, with +syntax and evaluation rules as :ref:`formatted string literals `, with the following differences: - Rather than evaluating to a ``str`` object, t-strings evaluate to a :class:`~string.templatelib.Template` object from the :mod:`string.templatelib` module. -- Evaluated expressions are *not* formatted using the - :func:`format` protocol; :meth:`~object.__format__` is *not* invoked. Instead, - the expressions are evaluated and a new :class:`~string.templatelib.Interpolation`` - object (also from the :mod:`string.templatelib` module) is created, which - contains the evaluated value of the expression. That ``Interpolation`` object - is found in the containing ``Template``. +- The :func:`format` protocol is not used. Instead, the format specifier and + conversions (if any) are passed to a new :class:`~string.templatelib.Interpolation` + object that is created for each evaluated expression. +- Format specifiers containing nested replacement fields are evaluated eagerly, + prior to being passed to the :class:`~string.templatelib.Interpolation` object. .. _numbers: diff --git a/Doc/sphinx-warnings.txt b/Doc/sphinx-warnings.txt new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 9a0a30104c9751a0bc243ddec6acebe4c3657ace Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 26 Jun 2025 20:36:28 +0000 Subject: [PATCH 14/25] Wrap up lexical analysis updates... I think? --- Doc/reference/lexical_analysis.rst | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index 7f26eb3c9245c4..fc385a61cd36b9 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -932,11 +932,24 @@ the following differences: - The :func:`format` protocol is not used. Instead, the format specifier and conversions (if any) are passed to a new :class:`~string.templatelib.Interpolation` - object that is created for each evaluated expression. + object that is created for each evaluated expression. It is up to code that + processes the resulting :class:`~string.templatelib.Template` object to + decide how to handle format specifiers and conversions. - Format specifiers containing nested replacement fields are evaluated eagerly, prior to being passed to the :class:`~string.templatelib.Interpolation` object. +- When the equal sign ``'='`` is provided in an interpolation expression, the + resulting :class:`~string.templatelib.Template` object will have the expression + text along with a ``'='`` character placed in its + :attr:`~string.templatelib.Template.strings` attribute. The + :attr:`~string.templatelib.Template.interpolations` attribute will also + contain an ``Interpolation`` instance for the expression. By default, the + :attr:`~string.templatelib.Interpolation.conversion` attribute will be set to + ``'r'`` (i.e. :func:`repr`), unless there is a conversion explicitly specified + (in which case it overrides the default) or a format specifier is provided (in + which case, the ``conversion`` defaults to ``None``). + .. _numbers: @@ -1069,7 +1082,7 @@ readability:: Either of these parts, but not both, can be empty. For example:: - 10. # (equivalent to 10.0) + 1. # (equivalent to 10.0) .001 # (equivalent to 0.001) Optionally, the integer and fraction may be followed by an *exponent*: From f2e5ca40137a59c0cb662374cd9b09b622938cf8 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 26 Jun 2025 21:04:40 +0000 Subject: [PATCH 15/25] Document the new AST nodes (TemplateStr and Interpolation) --- Doc/library/ast.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index ef6c62dca1e124..cf589b5af875ac 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -324,6 +324,49 @@ Literals values=[ Constant(value='.3')]))])) +.. class:: TemplateStr(values) + + A t-string, comprising a series of :class:`Interpolation` and :class:`Constant` + nodes. + + .. doctest:: + + >>> print(ast.dump(ast.parse('t"{name} finished {place:ordinal}"', mode='eval'), indent=4)) + Expression( + body=TemplateStr( + values=[ + Interpolation( + value=Name(id='name'), + str='name', + conversion=-1), + Constant(value=' finished '), + Interpolation( + value=Name(id='place'), + str='place', + conversion=-1, + format_spec=JoinedStr( + values=[ + Constant(value='ordinal')]))])) + + +.. class:: Interpolation(value, str, int, format_spec) + + Node representing a single interpolation field in a t-string. + + * ``value`` is any expression node (such as a literal, a variable, or a + function call). + * ``str`` is a constant containing the text of the interpolation expression. + * ``conversion`` is an integer: + + * -1: no conversion + * 115: ``!s`` string conversion + * 114: ``!r`` repr conversion + * 97: ``!a`` ascii conversion + + * ``format_spec`` is a :class:`JoinedStr` node representing the formatting + of the value, or ``None`` if no format was specified. Both + ``conversion`` and ``format_spec`` can be set at the same time. + .. class:: List(elts, ctx) Tuple(elts, ctx) From 8484b8166cbe329f81de814f0e9659ef79ca3d04 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 26 Jun 2025 22:10:28 +0000 Subject: [PATCH 16/25] First pass at documenting BUILD_TEMPLATE and BUILD_INTERPOLATION --- Doc/library/dis.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 11685a32f48e4f..170dbfee1b8f03 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -1120,6 +1120,45 @@ iterations of the loop. .. versionadded:: 3.12 +.. opcode:: BUILD_TEMPLATE + + Constructs a new :class:`~string.templatelib.Template` from a tuple + of strings and a tuple of interpolations and pushes the resulting instance + onto the stack:: + + interpolations = STACK.pop() + strings = STACK.pop() + STACK.append(_build_template(strings, interpolations)) + + .. versionadded:: 3.14 + + +.. opcode:: BUILD_INTERPOLATION (format) + + Constructs a new :class:`~string.templatelib.Interpolation` from an + expression and its source text and pushes the resulting instance onto the + stack. + + If the low bit of ``format`` is set, it indicates that the interpolation + contains a format specification. + + If ``format >> 2`` is non-zero, it indicates that the interpolation + contains a conversion. The value of ``format >> 2`` is the conversion type + (e.g. ``0`` for no conversion, ``1`` for ``!s``, ``2`` for ``!r``, and + ``3`` for ``!a``):: + + if format & 1: + format_spec = STACK.pop() + else: + format_spec = None + conversion = format >> 2 + expression = STACK.pop() + value = STACK.pop() + STACK.append(_build_interpolation(value, expression, conversion, format_spec)) + + .. versionadded:: 3.14 + + .. opcode:: BUILD_TUPLE (count) Creates a tuple consuming *count* items from the stack, and pushes the From 5a8cf1b1f1534050631219530d07c31b46d5190e Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 16:58:12 +0000 Subject: [PATCH 17/25] Clarify distinction between string.Template and string.templatelib.Template --- Doc/library/stdtypes.rst | 6 +++--- Doc/library/string.rst | 16 +++++++++++++--- Doc/library/string.templatelib.rst | 11 +++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 394c302fd354b9..cce38d3a48ca6e 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -2673,9 +2673,9 @@ For example: lead to a number of common errors (such as failing to display tuples and dictionaries correctly). Using the newer :ref:`formatted string literals `, the :meth:`str.format` interface, or :ref:`template strings - ` may help avoid these errors. Each of these - alternatives provides their own trade-offs and benefits of simplicity, - flexibility, and/or extensibility. + ($-strings) ` may help avoid these errors. + Each of these alternatives provides their own trade-offs and benefits of + simplicity, flexibility, and/or extensibility. String objects have one unique built-in operation: the ``%`` operator (modulo). This is also known as the string *formatting* or *interpolation* operator. diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 614b714c2baf52..ef90ef5dc86d6c 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -790,10 +790,20 @@ Nesting arguments and more complex examples:: -.. _template-strings: +.. _template-strings-pep292: -Template strings ----------------- +Template strings ($-strings) +---------------------------- + +.. note:: + + The :class:`~string.Template` class described here was introduced in Python + 2.4. It is entirely unrelated to, and should *not* be confused with, the + new :ref:`Template Strings ` feature and + :ref:`t-string literal syntax ` introduced in Python 3.14 and + originally described in :pep:`750`. Python's t-string literals evaluate to + instances of a different :class:`~string.templatelib.Template` class, which + is found in the :mod:`string.templatelib` module. Template strings provide simpler string substitutions as described in :pep:`292`. A primary use case for template strings is for diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index 8b9fed96008e71..c5bf49e8de1e65 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -14,6 +14,17 @@ :ref:`Format strings ` + +.. _template-strings: + +Template strings +---------------- + +Documentation forthcoming for PEP 750 template strings, also known as t-strings. + +.. versionadded:: 3.14 + + .. _templatelib-template: Template From 127ebc62218363dd1f0f52f887a9fa6934a8e86d Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 17:05:11 +0000 Subject: [PATCH 18/25] Simplify the NOTE --- Doc/library/string.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index ef90ef5dc86d6c..cab1094a03628c 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -797,13 +797,13 @@ Template strings ($-strings) .. note:: - The :class:`~string.Template` class described here was introduced in Python - 2.4. It is entirely unrelated to, and should *not* be confused with, the - new :ref:`Template Strings ` feature and - :ref:`t-string literal syntax ` introduced in Python 3.14 and - originally described in :pep:`750`. Python's t-string literals evaluate to - instances of a different :class:`~string.templatelib.Template` class, which - is found in the :mod:`string.templatelib` module. + The feature described here was introduced in Python 2.4. It is entirely + unrelated to, and should *not* be confused with, the new + :ref:`Template Strings ` feature and + :ref:`t-string literal syntax ` introduced in Python 3.14. + T-string literals evaluate to instances of a different + :class:`~string.templatelib.Template` class, found in the + :mod:`string.templatelib` module. Template strings provide simpler string substitutions as described in :pep:`292`. A primary use case for template strings is for From 73e12226dca4b576ad10fe97688c8ea4e0fba693 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 17:14:12 +0000 Subject: [PATCH 19/25] Okay, I'll stop wordsmithing this for now --- Doc/library/string.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index cab1094a03628c..dc10351857917c 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -797,8 +797,8 @@ Template strings ($-strings) .. note:: - The feature described here was introduced in Python 2.4. It is entirely - unrelated to, and should *not* be confused with, the new + The feature described here was introduced in Python 2.4. It is unrelated + to, and should not be confused with, the newer :ref:`Template Strings ` feature and :ref:`t-string literal syntax ` introduced in Python 3.14. T-string literals evaluate to instances of a different From 530cb6d863819b59c3400812547984e57e70e9a5 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 17:19:31 +0000 Subject: [PATCH 20/25] Fix formatting/parameters for TemplateStr/Interpolation AST nodes --- Doc/library/ast.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index cf589b5af875ac..44ee8d26b6ae86 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -326,8 +326,8 @@ Literals .. class:: TemplateStr(values) - A t-string, comprising a series of :class:`Interpolation` and :class:`Constant` - nodes. + A t-string, comprising a series of :class:`Interpolation` and :class:`Constant` + nodes. .. doctest:: @@ -349,23 +349,23 @@ Literals Constant(value='ordinal')]))])) -.. class:: Interpolation(value, str, int, format_spec) +.. class:: Interpolation(value, str, conversion, format_spec) - Node representing a single interpolation field in a t-string. + Node representing a single interpolation field in a t-string. - * ``value`` is any expression node (such as a literal, a variable, or a - function call). - * ``str`` is a constant containing the text of the interpolation expression. - * ``conversion`` is an integer: + * ``value`` is any expression node (such as a literal, a variable, or a + function call). + * ``str`` is a constant containing the text of the interpolation expression. + * ``conversion`` is an integer: - * -1: no conversion - * 115: ``!s`` string conversion - * 114: ``!r`` repr conversion - * 97: ``!a`` ascii conversion + * -1: no conversion + * 115: ``!s`` string conversion + * 114: ``!r`` repr conversion + * 97: ``!a`` ascii conversion - * ``format_spec`` is a :class:`JoinedStr` node representing the formatting - of the value, or ``None`` if no format was specified. Both - ``conversion`` and ``format_spec`` can be set at the same time. + * ``format_spec`` is a :class:`JoinedStr` node representing the formatting + of the value, or ``None`` if no format was specified. Both + ``conversion`` and ``format_spec`` can be set at the same time. .. class:: List(elts, ctx) From eaec5347fd57f8c8e44f406ba1b39fea37c75e31 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 17:25:39 +0000 Subject: [PATCH 21/25] Further ast/dis documentation cleanup --- Doc/library/ast.rst | 5 +++++ Doc/library/dis.rst | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 44ee8d26b6ae86..efab63afbf58e8 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -324,6 +324,7 @@ Literals values=[ Constant(value='.3')]))])) + .. class:: TemplateStr(values) A t-string, comprising a series of :class:`Interpolation` and :class:`Constant` @@ -348,6 +349,8 @@ Literals values=[ Constant(value='ordinal')]))])) + .. versionadded:: 3.14 + .. class:: Interpolation(value, str, conversion, format_spec) @@ -367,6 +370,8 @@ Literals of the value, or ``None`` if no format was specified. Both ``conversion`` and ``format_spec`` can be set at the same time. + .. versionadded:: 3.14 + .. class:: List(elts, ctx) Tuple(elts, ctx) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 170dbfee1b8f03..9a2d2d7b7a7235 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -1139,6 +1139,9 @@ iterations of the loop. expression and its source text and pushes the resulting instance onto the stack. + If no conversion or format specification is present, ``format`` is set to + ``2``. + If the low bit of ``format`` is set, it indicates that the interpolation contains a format specification. @@ -1147,11 +1150,11 @@ iterations of the loop. (e.g. ``0`` for no conversion, ``1`` for ``!s``, ``2`` for ``!r``, and ``3`` for ``!a``):: + conversion = format >> 2 if format & 1: format_spec = STACK.pop() else: format_spec = None - conversion = format >> 2 expression = STACK.pop() value = STACK.pop() STACK.append(_build_interpolation(value, expression, conversion, format_spec)) From 05b5beb5d6877110c54287b181cefa2ad3c24f66 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 20:01:34 +0000 Subject: [PATCH 22/25] Substantial updates to string.templatelib.rst docs --- Doc/library/string.templatelib.rst | 229 ++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 67 deletions(-) diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index c5bf49e8de1e65..9d9affc4250406 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -20,34 +20,60 @@ Template strings ---------------- -Documentation forthcoming for PEP 750 template strings, also known as t-strings. - .. versionadded:: 3.14 +Template strings are a generalization of :ref:`f-strings ` +that allow for powerful string processing. The :class:`Template` class +provides direct access to the static and interpolated (substituted) parts of +a string. -.. _templatelib-template: - -Template --------- - -The :class:`!Template` class describes the contents of a template string. - -The most common way to create a new :class:`!Template` instance is to use the t-string literal syntax. This syntax is identical to that of :ref:`f-strings`, except that the string is prefixed with a ``t`` instead of an ``f``. For example, the following code creates a :class:`Template` that can be used to format strings: +The most common way to create a :class:`!Template` instance is to use the +:ref:`t-string literal syntax `. This syntax is identical to that of +:ref:`f-strings`, except that the string is prefixed with a ``t`` instead of +an ``f``. For example, the following code creates a :class:`!Template`: >>> name = "World" >>> greeting = t"Hello {name}!" >>> type(greeting) - >>> print(list(greeting)) + >>> list(greeting) ['Hello ', Interpolation('World', 'name', None, ''), '!'] -It is also possible to create a :class:`!Template` directly, using its constructor. This takes an arbitrary collection of strings and :class:`Interpolation` instances: +The :class:`Interpolation` class represents an expression inside a template +string. It contains the evaluated value of the interpolation (``'World'`` in +this example), the original expression text (``'name'``), and optional +conversion and format specification attributes. - >>> from string.templatelib import Interpolation, Template +Templates can be processed in a variety of ways. For instance, here's a +simple example that converts static strings to lowercase and interpolated +values to uppercase: + + >>> from string.templatelib import Template + >>> def lower_upper(template: Template) -> str: + ... return ''.join( + ... part.lower() if isinstance(part, str) else part.value.upper() + ... for part in template + ... ) + ... >>> name = "World" - >>> greeting = Template("Hello, ", Interpolation(name, "name"), "!") - >>> print(list(greeting)) - ['Hello, ', Interpolation('World', 'name', None, ''), '!'] + >>> greeting = t"Hello {name}!" + >>> lower_upper(greeting) + 'hello WORLD!' + +More interesting use cases include sanitizing user input (e.g., to prevent SQL +injection or cross-site scripting attacks) or processing domain specific +languages. + + +.. _templatelib-template: + +Template +-------- + +The :class:`!Template` class describes the contents of a template string. + +:class:`!Template` instances are shallow immutable: their attributes cannot be +reassigned. .. class:: Template(*args) @@ -56,18 +82,28 @@ It is also possible to create a :class:`!Template` directly, using its construct :param args: A mix of strings and :class:`Interpolation` instances in any order. :type args: str | Interpolation + While :ref:`t-string literal syntax ` is the most common way to + create :class:`!Template` instances, it is also possible to create them + directly using the constructor: + + >>> from string.templatelib import Interpolation, Template + >>> name = "World" + >>> greeting = Template("Hello, ", Interpolation(name, "name"), "!") + >>> list(greeting) + ['Hello, ', Interpolation('World', 'name', None, ''), '!'] + If two or more consecutive strings are passed, they will be concatenated into a single value in the :attr:`~Template.strings` attribute. For example, the following code creates a :class:`Template` with a single final string: >>> from string.templatelib import Template >>> greeting = Template("Hello ", "World", "!") - >>> print(greeting.strings) + >>> greeting.strings ('Hello World!',) If two or more consecutive interpolations are passed, they will be treated as separate interpolations and an empty string will be inserted between them. For example, the following code creates a template with a single value in the :attr:`~Template.strings` attribute: >>> from string.templatelib import Interpolation, Template >>> greeting = Template(Interpolation("World", "name"), Interpolation("!", "punctuation")) - >>> print(greeting.strings) + >>> greeting.strings ('', '', '') .. attribute:: strings @@ -76,24 +112,39 @@ It is also possible to create a :class:`!Template` directly, using its construct A :ref:`tuple ` of the static strings in the template. >>> name = "World" - >>> print(t"Hello {name}!".strings) + >>> t"Hello {name}!".strings ('Hello ', '!') Empty strings *are* included in the tuple: >>> name = "World" - >>> print(t"Hello {name}{name}!".strings) + >>> t"Hello {name}{name}!".strings ('Hello ', '', '!') + The ``strings`` tuple is never empty, and always contains one more + string than the ``interpolations`` and ``values`` tuples: + + >>> t"".strings + ('',) + >>> t"{'cheese'}".strings + ('', '') + >>> t"{'cheese'}".values + ('cheese',) + .. attribute:: interpolations :type: tuple[Interpolation, ...] A tuple of the interpolations in the template. >>> name = "World" - >>> print(t"Hello {name}!".interpolations) + >>> t"Hello {name}!".interpolations (Interpolation('World', 'name', None, ''),) + The ``interpolations`` tuple may be empty and always contains one fewer + values than the ``strings`` tuple: + + >>> t"Hello!".interpolations + () .. attribute:: values :type: tuple[Any, ...] @@ -101,95 +152,139 @@ It is also possible to create a :class:`!Template` directly, using its construct A tuple of all interpolated values in the template. >>> name = "World" - >>> print(t"Hello {name}!".values) + >>> t"Hello {name}!".values ('World',) + The ``values`` tuple is always the same length as the + ``interpolations`` tuple. + .. method:: __iter__() - Iterate over the template, yielding each string and :class:`Interpolation` in order. + Iterate over the template, yielding each string and + :class:`Interpolation` in order. >>> name = "World" - >>> print(list(t"Hello {name}!")) + >>> list(t"Hello {name}!") ['Hello ', Interpolation('World', 'name', None, ''), '!'] Empty strings are *not* included in the iteration: >>> name = "World" - >>> print(list(t"Hello {name}{name}")) + >>> list(t"Hello {name}{name}") ['Hello ', Interpolation('World', 'name', None, ''), Interpolation('World', 'name', None, '')] - :returns: An iterable of all the parts in the template. - :rtype: typing.Iterator[str | Interpolation] + .. method:: __add__(other) + + Concatenate this template with another template, returning a new + :class:`!Template` instance: + + >>> name = "World" + >>> list(t"Hello " + t"there {name}!") + ['Hello there ', Interpolation('World', 'name', None, ''), '!'] + + Concatenation between a :class:`!Template` and a ``str`` is *not* supported. + This is because it is ambiguous whether the string should be treated as + a static string or an interpolation. If you want to concatenate a + :class:`!Template` with a string, you should either wrap the string + directly in a :class:`!Template` (to treat it as a static string) or use + an :class:`!Interpolation` (to treat it as dynamic): + + >>> from string.templatelib import Template, Interpolation + >>> greeting = t"Hello " + >>> greeting += Template("there ") # Treat as static + >>> name = "World" + >>> greeting += Template(Interpolation(name, "name")) # Treat as dynamic + >>> list(greeting) + ['Hello there ', Interpolation('World', 'name', None, '')] + -.. class:: Interpolation(*args) +.. class:: Interpolation(value, expression="", conversion=None, format_spec="") Create a new :class:`!Interpolation` object. :param value: The evaluated, in-scope result of the interpolation. :type value: object - :param expression: The original *text* of the interpolation's Python :ref:`expressions `. + :param expression: The text of a valid Python expression, or an empty string :type expression: str :param conversion: The optional :ref:`conversion ` to be used, one of r, s, and a,. - :type value: Literal["a", "r", "s"] | None + :type conversion: Literal["a", "r", "s"] | None :param format_spec: An optional, arbitrary string used as the :ref:`format specification ` to present the value. + :type format_spec: str - The :class:`!Interpolation` type represents an expression inside a template string. It is shallow immutable -- its attributes cannot be reassigned. + The :class:`!Interpolation` type represents an expression inside a template string. - >>> name = "World" - >>> template = t"Hello {name}" - >>> template.interpolations[0].value - 'World' - >>> template.interpolations[0].value = "Galaxy" - Traceback (most recent call last): - File "", line 1, in - AttributeError: readonly attribute + :class:`!Interpolation` instances are shallow immutable: their attributes cannot be + reassigned. - While f-strings and t-strings are largely similar in syntax and expectations, the :attr:`~Interpolation.conversion` and :attr:`~Interpolation.format_spec` behave differently. With f-strings, these are applied to the resulting value automatically. For example, in this ``format_spec``: + .. property:: value - >>> value = 42 - >>> f"Value: {value:.2f}" - 'Value: 42.00' + :returns: The evaluated value of the interpolation. + :rtype: object - With a t-string :class:`!Interpolation`, the template function is expected to apply this to the value: + >>> t"{1 + 2}".interpolations[0].value + 3 - >>> value = 42 - >>> template = t"Value: {value:.2f}" - >>> template.interpolations[0].value - 42 + .. property:: expression - .. property:: __match_args__ + :returns: The text of a valid Python expression, or an empty string. + :rtype: str - :returns: A tuple of the attributes to use for structural pattern matching. - :rtype: (Literal["value"], Literal["expression"], Literal["conversion"], Literal["format_spec"]) + The :attr:`~Interpolation.expression` is the original text of the + interpolation's Python expression, if the interpolation was created + from a t-string literal. Developers creating interpolations manually + should either set this to an empty string or choose a suitable valid + Python expression. + >>> t"{1 + 2}".interpolations[0].expression + '1 + 2' - .. property:: value + .. property:: conversion - :returns: The evaluated value of the interpolation. - :rtype: object + :returns: The conversion to apply to the value, or ``None`` + :rtype: Literal["a", "r", "s"] | None - .. property:: expression + The :attr:`!Interpolation.conversion` is the optional conversion to apply + to the value: + + >>> t"{1 + 2!a}".interpolations[0].conversion + 'a' + + .. note:: + + Unlike f-strings, where conversions are applied automatically, + the expected behavior with t-strings is that code that *processes* the + :class:`!Template` will decide how to interpret and whether to apply + the :attr:`!Interpolation.conversion`. - :returns: The original text of the interpolation's Python expression if the interpolation was created from a t-string literal + .. property:: format_spec + + :returns: The format specification to apply to the value. :rtype: str - The :attr:`~Interpolation.expression` is the original text of the interpolation's Python expression, if the interpolation was created from a t-string literal. Developers creating - interpolations manually should either set this to an empty - string or choose a suitable valid python expression. + The :attr:`!Interpolation.format_spec` is an optional, arbitrary string + used as the format specification to present the value: - .. property:: conversion + >>> t"{1 + 2:.2f}".interpolations[0].format_spec + '.2f' - :returns: The conversion to apply to the value, one of "a", "r", or "s", or None. - :rtype: Literal["a", "r", "s"] | None + .. note:: - The :attr:`~Interpolation.conversion` is the optional conversion to apply to the value. This is one of "a", "r", or "s", or None if no conversion is specified. + Unlike f-strings, where format specifications are applied automatically + via the :func:`format` protocol, the expected behavior with + t-strings is that code that *processes* the :class:`!Template` will + decide how to interpret and whether to apply the format specification. + As a result, :attr:`!Interpolation.format_spec` values in + :class:`!Template` instances can be arbitrary strings, even those that + do not necessarily conform to the rules of Python's :func:`format` + protocol. - .. property:: format_spec + .. property:: __match_args__ + + :returns: A tuple of the attributes to use for structural pattern matching. - :returns: The format specification to apply to the value. - :rtype: str + The tuple returned is ``('value', 'expression', 'conversion', 'format_spec')``. + This allows for :ref:`pattern matching ` on :class:`!Interpolation` instances. - The :attr:`~Interpolation.format_spec` is an optional, arbitrary string used as the format specification to present the value. This is similar to the format specification used in :ref:`format strings `, but it is not limited to a specific set of formats. From 680189ac5030c2b0afd2b6dc31883137868abadd Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Jun 2025 22:24:56 +0000 Subject: [PATCH 23/25] Clarify BUILD_INTERPOLATION behavior --- Doc/library/dis.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 9a2d2d7b7a7235..6e5f65e122dcdb 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -1135,8 +1135,8 @@ iterations of the loop. .. opcode:: BUILD_INTERPOLATION (format) - Constructs a new :class:`~string.templatelib.Interpolation` from an - expression and its source text and pushes the resulting instance onto the + Constructs a new :class:`~string.templatelib.Interpolation` from a + value and its source expression and pushes the resulting instance onto the stack. If no conversion or format specification is present, ``format`` is set to From 71ddbf46de6aa4011a9e997fcb8bb93c136355b1 Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 29 Jun 2025 23:05:10 +0000 Subject: [PATCH 24/25] Write a template string "tutorial" in the "fancy input output formatting" --- Doc/library/string.rst | 2 +- Doc/library/string.templatelib.rst | 102 ++++++-------- Doc/tutorial/inputoutput.rst | 206 ++++++++++++++++++++++++++++- 3 files changed, 246 insertions(+), 64 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index dc10351857917c..7b624d18d7f8e0 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -799,7 +799,7 @@ Template strings ($-strings) The feature described here was introduced in Python 2.4. It is unrelated to, and should not be confused with, the newer - :ref:`Template Strings ` feature and + :ref:`template strings ` feature and :ref:`t-string literal syntax ` introduced in Python 3.14. T-string literals evaluate to instances of a different :class:`~string.templatelib.Template` class, found in the diff --git a/Doc/library/string.templatelib.rst b/Doc/library/string.templatelib.rst index 9d9affc4250406..ed2e9e3f7f84b7 100644 --- a/Doc/library/string.templatelib.rst +++ b/Doc/library/string.templatelib.rst @@ -11,8 +11,9 @@ .. seealso:: + :ref:`T-strings tutorial ` :ref:`Format strings ` - + :ref:`T-string literal syntax ` .. _template-strings: @@ -22,47 +23,12 @@ Template strings .. versionadded:: 3.14 -Template strings are a generalization of :ref:`f-strings ` -that allow for powerful string processing. The :class:`Template` class -provides direct access to the static and interpolated (substituted) parts of -a string. - -The most common way to create a :class:`!Template` instance is to use the -:ref:`t-string literal syntax `. This syntax is identical to that of -:ref:`f-strings`, except that the string is prefixed with a ``t`` instead of -an ``f``. For example, the following code creates a :class:`!Template`: - - >>> name = "World" - >>> greeting = t"Hello {name}!" - >>> type(greeting) - - >>> list(greeting) - ['Hello ', Interpolation('World', 'name', None, ''), '!'] - -The :class:`Interpolation` class represents an expression inside a template -string. It contains the evaluated value of the interpolation (``'World'`` in -this example), the original expression text (``'name'``), and optional -conversion and format specification attributes. - -Templates can be processed in a variety of ways. For instance, here's a -simple example that converts static strings to lowercase and interpolated -values to uppercase: +Template strings are an extension of :ref:`f-strings ` +that allow for greater control of formatting behavior. The :class:`Template` +class gives you access to the static and interpolated (in curly braces) +parts of a string *before* they are combined into a final string. - >>> from string.templatelib import Template - >>> def lower_upper(template: Template) -> str: - ... return ''.join( - ... part.lower() if isinstance(part, str) else part.value.upper() - ... for part in template - ... ) - ... - >>> name = "World" - >>> greeting = t"Hello {name}!" - >>> lower_upper(greeting) - 'hello WORLD!' - -More interesting use cases include sanitizing user input (e.g., to prevent SQL -injection or cross-site scripting attacks) or processing domain specific -languages. +See the :ref:`t-strings tutorial ` for an introduction. .. _templatelib-template: @@ -82,28 +48,45 @@ reassigned. :param args: A mix of strings and :class:`Interpolation` instances in any order. :type args: str | Interpolation - While :ref:`t-string literal syntax ` is the most common way to - create :class:`!Template` instances, it is also possible to create them - directly using the constructor: + The most common way to create a :class:`!Template` instance is to use the + :ref:`t-string literal syntax `. This syntax is identical to that of + :ref:`f-strings` except that it uses a ``t`` instead of an ``f``: + + >>> name = "World" + >>> template = t"Hello {name}!" + >>> type(template) + + >>> template.strings + ('Hello ', '!') + >>> template.values + ('World',) + + While literal syntax is the most common way to create :class:`!Template` + instances, it is also possible to create them directly using the constructor: >>> from string.templatelib import Interpolation, Template >>> name = "World" - >>> greeting = Template("Hello, ", Interpolation(name, "name"), "!") - >>> list(greeting) + >>> template = Template("Hello, ", Interpolation(name, "name"), "!") + >>> list(template) ['Hello, ', Interpolation('World', 'name', None, ''), '!'] - If two or more consecutive strings are passed, they will be concatenated into a single value in the :attr:`~Template.strings` attribute. For example, the following code creates a :class:`Template` with a single final string: + If two or more consecutive strings are passed, they will be concatenated + into a single value in the :attr:`~Template.strings` attribute. For example, + the following code creates a :class:`Template` with a single final string: >>> from string.templatelib import Template - >>> greeting = Template("Hello ", "World", "!") - >>> greeting.strings + >>> template = Template("Hello ", "World", "!") + >>> template.strings ('Hello World!',) - If two or more consecutive interpolations are passed, they will be treated as separate interpolations and an empty string will be inserted between them. For example, the following code creates a template with a single value in the :attr:`~Template.strings` attribute: + If two or more consecutive interpolations are passed, they will be treated + as separate interpolations and an empty string will be inserted between them. + For example, the following code creates a template with a single value in + the :attr:`~Template.strings` attribute: >>> from string.templatelib import Interpolation, Template - >>> greeting = Template(Interpolation("World", "name"), Interpolation("!", "punctuation")) - >>> greeting.strings + >>> template = Template(Interpolation("World", "name"), Interpolation("!", "punctuation")) + >>> template.strings ('', '', '') .. attribute:: strings @@ -156,7 +139,8 @@ reassigned. ('World',) The ``values`` tuple is always the same length as the - ``interpolations`` tuple. + ``interpolations`` tuple. It is equivalent to + ``tuple(i.value for i in template.interpolations)``. .. method:: __iter__() @@ -175,7 +159,7 @@ reassigned. .. method:: __add__(other) - Concatenate this template with another template, returning a new + Concatenate this template with another, returning a new :class:`!Template` instance: >>> name = "World" @@ -190,11 +174,13 @@ reassigned. an :class:`!Interpolation` (to treat it as dynamic): >>> from string.templatelib import Template, Interpolation - >>> greeting = t"Hello " - >>> greeting += Template("there ") # Treat as static + >>> template = t"Hello " + >>> # Treat "there " as a static string + >>> template += Template("there ") + >>> # Treat name as an interpolation >>> name = "World" - >>> greeting += Template(Interpolation(name, "name")) # Treat as dynamic - >>> list(greeting) + >>> template += Template(Interpolation(name, "name")) + >>> list(template) ['Hello there ', Interpolation('World', 'name', None, '')] diff --git a/Doc/tutorial/inputoutput.rst b/Doc/tutorial/inputoutput.rst index 35b8c7cd8eb049..fd2b0786d764da 100644 --- a/Doc/tutorial/inputoutput.rst +++ b/Doc/tutorial/inputoutput.rst @@ -34,13 +34,28 @@ printing space-separated values. There are several ways to format output. >>> f'Results of the {year} {event}' 'Results of the 2016 Referendum' +* When greater control is needed, :ref:`template string literals ` + can be useful. T-strings -- which begin with ``t`` or ``T`` -- share the + same syntax as f-strings but, unlike f-strings, produce a + :class:`~string.templatelib.Template` instance rather than a simple ``str``. + Templates give you access to the static and interpolated (in curly braces) + parts of the string *before* they are combined into a final string. + + :: + + >>> name = "World" + >>> template = t"Hello {name}!" + >>> template.strings + ('Hello ', '!') + >>> template.values + ('World',) + * The :meth:`str.format` method of strings requires more manual effort. You'll still use ``{`` and ``}`` to mark where a variable will be substituted and can provide detailed formatting directives, but you'll also need to provide the information to be formatted. In the following code block there are two examples of how to format variables: - :: >>> yes_votes = 42_572_654 @@ -95,10 +110,11 @@ Some examples:: >>> repr((x, y, ('spam', 'eggs'))) "(32.5, 40000, ('spam', 'eggs'))" -The :mod:`string` module contains a :class:`~string.Template` class that offers -yet another way to substitute values into strings, using placeholders like -``$x`` and replacing them with values from a dictionary, but offers much less -control of the formatting. +The :mod:`string` module also contains support for so-called +:ref:`$-strings ` that offer yet another way to +substitute values into strings, using placeholders like ``$x`` and replacing +them with values from a dictionary. This syntax is easy to use, although +it offers much less control of the formatting. .. index:: single: formatted string literal @@ -160,6 +176,186 @@ See :ref:`self-documenting expressions ` for more informatio on the ``=`` specifier. For a reference on these format specifications, see the reference guide for the :ref:`formatspec`. + +.. _tut-t-strings: + +Template String Literals +------------------------- + +:ref:`Template string literals ` (also called t-strings for short) +are an extension of :ref:`f-strings ` that let you access the +static and interpolated parts of a string *before* they are combined into a +final string. This provides for greater control over how the string is +formatted. + +The most common way to create a :class:`~string.templatelib.Template` instance +is to use the :ref:`t-string literal syntax `. This syntax is +identical to that of :ref:`f-strings` except that it uses a ``t`` instead of +an ``f``: + + >>> name = "World" + >>> template = t"Hello {name}!" + >>> template.strings + ('Hello ', '!') + >>> template.values + ('World',) + +:class:`~!string.templatelib.Template` instances are iterable, yielding each +string and :class:`~string.templatelib.Interpolation` in order: + +.. testsetup:: + + name = "World" + template = t"Hello {name}!" + +.. doctest:: + + >>> list(template) + ['Hello ', Interpolation('World', 'name', None, ''), '!'] + +Interpolations represent expressions inside a t-string. They contain the +evaluated value of the expression (``'World'`` in this example), the text of +the original expression (``'name'``), and optional conversion and format +specification attributes. + +Templates can be processed in a variety of ways. For instance, here's code that +converts static strings to lowercase and interpolated values to uppercase: + + >>> from string.templatelib import Template + >>> + >>> def lower_upper(template: Template) -> str: + ... return ''.join( + ... part.lower() if isinstance(part, str) else part.value.upper() + ... for part in template + ... ) + ... + >>> name = "World" + >>> template = t"Hello {name}!" + >>> lower_upper(template) + 'hello WORLD!' + +Template strings are particularly useful for sanitizing user input. Imagine +we're building a web application that has user profile pages. Perhaps the +``User`` class is defined like this: + + >>> from dataclasses import dataclass + >>> + >>> @dataclass + ... class User: + ... name: str + ... + +Imagine using f-strings in to generate HTML for the ``User``: + +.. testsetup:: + + class User: + name: str + def __init__(self, name: str): + self.name = name + + +.. doctest:: + + >>> # Warning: this is dangerous code. Don't do this! + >>> def user_html(user: User) -> str: + ... return f"

{user.name}

" + ... + +This code is dangerous because our website lets users type in their own names. +If a user types in a name like ``""``, the +browser will execute that script when someone else visits their profile page. +This is called a *cross-site scripting (XSS) vulnerability*, and it is a form +of *injection vulnerability*. Injection vulnerabilities occur when user input +is included in a program without proper sanitization, allowing malicious code +to be executed. The same sorts of vulnerabilities can occur when user input is +included in SQL queries, command lines, or other contexts where the input is +interpreted as code. + +To prevent this, instead of using f-strings, we can use t-strings. Let's +update our ``user_html()`` function to return a :class:`~string.templatelib.Template`: + + >>> from string.templatelib import Template + >>> + >>> def user_html(user: User) -> Template: + ... return t"

{user.name}

" + +Now let's implement a function that sanitizes *any* HTML +:class:`~!string.templatelib.Template`: + + >>> from html import escape + >>> from string.templatelib import Template + >>> + >>> def sanitize_html_template(template: Template) -> str: + ... return ''.join( + ... part if isinstance(part, str) else escape(part.value) + ... for part in template + ... ) + ... + +This function iterates over the parts of the +:class:`~!string.templatelib.Template`, escaping any interpolated values using +the :func:`html.escape` function, which converts special characters like `<`, +`>`, and `&` into their HTML-safe equivalents. + +Now we can tie it all together: + +.. testsetup:: + + from dataclasses import dataclass + from string.templatelib import Template + from html import escape + @dataclass + class User: + name: str + def sanitize_html_template(template: Template) -> str: + return ''.join( + part if isinstance(part, str) else escape(part.value) + for part in template + ) + def user_html(user: User) -> Template: + return t"

{user.name}

" + +.. doctest:: + + >>> evil_user = User(name="") + >>> template = user_html(evil_user) + >>> safe = sanitize_html_template(template) + >>> print(safe) +

<script>alert('evil');</script>

+ +We are no longer vulnerable to XSS attacks because we are escaping the +interpolated values before they are included in the rendered HTML. + +Of course, there's no need for code that processes +:class:`~!string.templatelib.Template` instances to be limited to returning a +simple string. For instance, we could imagine defining a more complex ``html()`` +function that returns a structured representation of the HTML: + + >>> from dataclasses import dataclass + >>> from string.templatelib import Template + >>> from html.parser import HTMLParser + >>> + >>> @dataclass + ... class Element: + ... tag: str + ... attributes: dict[str, str] + ... children: list[str | Element] + ... + >>> def parse_html(template: Template) -> Element: + ... """ + ... Uses python's built-in HTMLParser to parse the template, + ... handle any interpolated values, and return a tree of + ... Element instances. + ... """ + ... pass + ... + +A full implementation of this function would be quite complex and is not +provided here. That said, the fact that it is possible to implement a method +like ``parse_html()`` showcases the flexibility and power of t-strings. + + .. _tut-string-format: The String format() Method From 1c0ed701e3f8c7f4c2b9a022cd0f0385bcd17b72 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 30 Jun 2025 02:27:20 +0000 Subject: [PATCH 25/25] Fix lint issue in inputoutput.rst --- Doc/tutorial/inputoutput.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/tutorial/inputoutput.rst b/Doc/tutorial/inputoutput.rst index fd2b0786d764da..c2f7b0bf29f2c8 100644 --- a/Doc/tutorial/inputoutput.rst +++ b/Doc/tutorial/inputoutput.rst @@ -295,8 +295,8 @@ Now let's implement a function that sanitizes *any* HTML This function iterates over the parts of the :class:`~!string.templatelib.Template`, escaping any interpolated values using -the :func:`html.escape` function, which converts special characters like `<`, -`>`, and `&` into their HTML-safe equivalents. +the :func:`html.escape` function, which converts special characters like ``<``, +``>``, and ``&`` into their HTML-safe equivalents. Now we can tie it all together: