Skip to content

Commit 71ddbf4

Browse files
committed
Write a template string "tutorial" in the "fancy input output formatting"
1 parent 680189a commit 71ddbf4

3 files changed

Lines changed: 246 additions & 64 deletions

File tree

Doc/library/string.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,7 @@ Template strings ($-strings)
799799

800800
The feature described here was introduced in Python 2.4. It is unrelated
801801
to, and should not be confused with, the newer
802-
:ref:`Template Strings <template-strings>` feature and
802+
:ref:`template strings <template-strings>` feature and
803803
:ref:`t-string literal syntax <t-strings>` introduced in Python 3.14.
804804
T-string literals evaluate to instances of a different
805805
:class:`~string.templatelib.Template` class, found in the

Doc/library/string.templatelib.rst

Lines changed: 44 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
.. seealso::
1313

14+
:ref:`T-strings tutorial <tut-t-strings>`
1415
:ref:`Format strings <f-strings>`
15-
16+
:ref:`T-string literal syntax <t-strings>`
1617

1718

1819
.. _template-strings:
@@ -22,47 +23,12 @@ Template strings
2223

2324
.. versionadded:: 3.14
2425

25-
Template strings are a generalization of :ref:`f-strings <f-strings>`
26-
that allow for powerful string processing. The :class:`Template` class
27-
provides direct access to the static and interpolated (substituted) parts of
28-
a string.
29-
30-
The most common way to create a :class:`!Template` instance is to use the
31-
:ref:`t-string literal syntax <t-strings>`. This syntax is identical to that of
32-
:ref:`f-strings`, except that the string is prefixed with a ``t`` instead of
33-
an ``f``. For example, the following code creates a :class:`!Template`:
34-
35-
>>> name = "World"
36-
>>> greeting = t"Hello {name}!"
37-
>>> type(greeting)
38-
<class 'string.templatelib.Template'>
39-
>>> list(greeting)
40-
['Hello ', Interpolation('World', 'name', None, ''), '!']
41-
42-
The :class:`Interpolation` class represents an expression inside a template
43-
string. It contains the evaluated value of the interpolation (``'World'`` in
44-
this example), the original expression text (``'name'``), and optional
45-
conversion and format specification attributes.
46-
47-
Templates can be processed in a variety of ways. For instance, here's a
48-
simple example that converts static strings to lowercase and interpolated
49-
values to uppercase:
26+
Template strings are an extension of :ref:`f-strings <f-strings>`
27+
that allow for greater control of formatting behavior. The :class:`Template`
28+
class gives you access to the static and interpolated (in curly braces)
29+
parts of a string *before* they are combined into a final string.
5030

51-
>>> from string.templatelib import Template
52-
>>> def lower_upper(template: Template) -> str:
53-
... return ''.join(
54-
... part.lower() if isinstance(part, str) else part.value.upper()
55-
... for part in template
56-
... )
57-
...
58-
>>> name = "World"
59-
>>> greeting = t"Hello {name}!"
60-
>>> lower_upper(greeting)
61-
'hello WORLD!'
62-
63-
More interesting use cases include sanitizing user input (e.g., to prevent SQL
64-
injection or cross-site scripting attacks) or processing domain specific
65-
languages.
31+
See the :ref:`t-strings tutorial <tut-t-strings>` for an introduction.
6632

6733

6834
.. _templatelib-template:
@@ -82,28 +48,45 @@ reassigned.
8248
:param args: A mix of strings and :class:`Interpolation` instances in any order.
8349
:type args: str | Interpolation
8450

85-
While :ref:`t-string literal syntax <t-strings>` is the most common way to
86-
create :class:`!Template` instances, it is also possible to create them
87-
directly using the constructor:
51+
The most common way to create a :class:`!Template` instance is to use the
52+
:ref:`t-string literal syntax <t-strings>`. This syntax is identical to that of
53+
:ref:`f-strings` except that it uses a ``t`` instead of an ``f``:
54+
55+
>>> name = "World"
56+
>>> template = t"Hello {name}!"
57+
>>> type(template)
58+
<class 'string.templatelib.Template'>
59+
>>> template.strings
60+
('Hello ', '!')
61+
>>> template.values
62+
('World',)
63+
64+
While literal syntax is the most common way to create :class:`!Template`
65+
instances, it is also possible to create them directly using the constructor:
8866

8967
>>> from string.templatelib import Interpolation, Template
9068
>>> name = "World"
91-
>>> greeting = Template("Hello, ", Interpolation(name, "name"), "!")
92-
>>> list(greeting)
69+
>>> template = Template("Hello, ", Interpolation(name, "name"), "!")
70+
>>> list(template)
9371
['Hello, ', Interpolation('World', 'name', None, ''), '!']
9472

95-
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:
73+
If two or more consecutive strings are passed, they will be concatenated
74+
into a single value in the :attr:`~Template.strings` attribute. For example,
75+
the following code creates a :class:`Template` with a single final string:
9676

9777
>>> from string.templatelib import Template
98-
>>> greeting = Template("Hello ", "World", "!")
99-
>>> greeting.strings
78+
>>> template = Template("Hello ", "World", "!")
79+
>>> template.strings
10080
('Hello World!',)
10181

102-
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:
82+
If two or more consecutive interpolations are passed, they will be treated
83+
as separate interpolations and an empty string will be inserted between them.
84+
For example, the following code creates a template with a single value in
85+
the :attr:`~Template.strings` attribute:
10386

10487
>>> from string.templatelib import Interpolation, Template
105-
>>> greeting = Template(Interpolation("World", "name"), Interpolation("!", "punctuation"))
106-
>>> greeting.strings
88+
>>> template = Template(Interpolation("World", "name"), Interpolation("!", "punctuation"))
89+
>>> template.strings
10790
('', '', '')
10891

10992
.. attribute:: strings
@@ -156,7 +139,8 @@ reassigned.
156139
('World',)
157140

158141
The ``values`` tuple is always the same length as the
159-
``interpolations`` tuple.
142+
``interpolations`` tuple. It is equivalent to
143+
``tuple(i.value for i in template.interpolations)``.
160144

161145
.. method:: __iter__()
162146

@@ -175,7 +159,7 @@ reassigned.
175159

176160
.. method:: __add__(other)
177161

178-
Concatenate this template with another template, returning a new
162+
Concatenate this template with another, returning a new
179163
:class:`!Template` instance:
180164

181165
>>> name = "World"
@@ -190,11 +174,13 @@ reassigned.
190174
an :class:`!Interpolation` (to treat it as dynamic):
191175

192176
>>> from string.templatelib import Template, Interpolation
193-
>>> greeting = t"Hello "
194-
>>> greeting += Template("there ") # Treat as static
177+
>>> template = t"Hello "
178+
>>> # Treat "there " as a static string
179+
>>> template += Template("there ")
180+
>>> # Treat name as an interpolation
195181
>>> name = "World"
196-
>>> greeting += Template(Interpolation(name, "name")) # Treat as dynamic
197-
>>> list(greeting)
182+
>>> template += Template(Interpolation(name, "name"))
183+
>>> list(template)
198184
['Hello there ', Interpolation('World', 'name', None, '')]
199185

200186

Doc/tutorial/inputoutput.rst

Lines changed: 201 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,28 @@ printing space-separated values. There are several ways to format output.
3434
>>> f'Results of the {year} {event}'
3535
'Results of the 2016 Referendum'
3636

37+
* When greater control is needed, :ref:`template string literals <tut-t-strings>`
38+
can be useful. T-strings -- which begin with ``t`` or ``T`` -- share the
39+
same syntax as f-strings but, unlike f-strings, produce a
40+
:class:`~string.templatelib.Template` instance rather than a simple ``str``.
41+
Templates give you access to the static and interpolated (in curly braces)
42+
parts of the string *before* they are combined into a final string.
43+
44+
::
45+
46+
>>> name = "World"
47+
>>> template = t"Hello {name}!"
48+
>>> template.strings
49+
('Hello ', '!')
50+
>>> template.values
51+
('World',)
52+
3753
* The :meth:`str.format` method of strings requires more manual
3854
effort. You'll still use ``{`` and ``}`` to mark where a variable
3955
will be substituted and can provide detailed formatting directives,
4056
but you'll also need to provide the information to be formatted. In the following code
4157
block there are two examples of how to format variables:
4258

43-
4459
::
4560

4661
>>> yes_votes = 42_572_654
@@ -95,10 +110,11 @@ Some examples::
95110
>>> repr((x, y, ('spam', 'eggs')))
96111
"(32.5, 40000, ('spam', 'eggs'))"
97112

98-
The :mod:`string` module contains a :class:`~string.Template` class that offers
99-
yet another way to substitute values into strings, using placeholders like
100-
``$x`` and replacing them with values from a dictionary, but offers much less
101-
control of the formatting.
113+
The :mod:`string` module also contains support for so-called
114+
:ref:`$-strings <template-strings-pep292>` that offer yet another way to
115+
substitute values into strings, using placeholders like ``$x`` and replacing
116+
them with values from a dictionary. This syntax is easy to use, although
117+
it offers much less control of the formatting.
102118

103119
.. index::
104120
single: formatted string literal
@@ -160,6 +176,186 @@ See :ref:`self-documenting expressions <bpo-36817-whatsnew>` for more informatio
160176
on the ``=`` specifier. For a reference on these format specifications, see
161177
the reference guide for the :ref:`formatspec`.
162178

179+
180+
.. _tut-t-strings:
181+
182+
Template String Literals
183+
-------------------------
184+
185+
:ref:`Template string literals <t-strings>` (also called t-strings for short)
186+
are an extension of :ref:`f-strings <tut-f-strings>` that let you access the
187+
static and interpolated parts of a string *before* they are combined into a
188+
final string. This provides for greater control over how the string is
189+
formatted.
190+
191+
The most common way to create a :class:`~string.templatelib.Template` instance
192+
is to use the :ref:`t-string literal syntax <t-strings>`. This syntax is
193+
identical to that of :ref:`f-strings` except that it uses a ``t`` instead of
194+
an ``f``:
195+
196+
>>> name = "World"
197+
>>> template = t"Hello {name}!"
198+
>>> template.strings
199+
('Hello ', '!')
200+
>>> template.values
201+
('World',)
202+
203+
:class:`~!string.templatelib.Template` instances are iterable, yielding each
204+
string and :class:`~string.templatelib.Interpolation` in order:
205+
206+
.. testsetup::
207+
208+
name = "World"
209+
template = t"Hello {name}!"
210+
211+
.. doctest::
212+
213+
>>> list(template)
214+
['Hello ', Interpolation('World', 'name', None, ''), '!']
215+
216+
Interpolations represent expressions inside a t-string. They contain the
217+
evaluated value of the expression (``'World'`` in this example), the text of
218+
the original expression (``'name'``), and optional conversion and format
219+
specification attributes.
220+
221+
Templates can be processed in a variety of ways. For instance, here's code that
222+
converts static strings to lowercase and interpolated values to uppercase:
223+
224+
>>> from string.templatelib import Template
225+
>>>
226+
>>> def lower_upper(template: Template) -> str:
227+
... return ''.join(
228+
... part.lower() if isinstance(part, str) else part.value.upper()
229+
... for part in template
230+
... )
231+
...
232+
>>> name = "World"
233+
>>> template = t"Hello {name}!"
234+
>>> lower_upper(template)
235+
'hello WORLD!'
236+
237+
Template strings are particularly useful for sanitizing user input. Imagine
238+
we're building a web application that has user profile pages. Perhaps the
239+
``User`` class is defined like this:
240+
241+
>>> from dataclasses import dataclass
242+
>>>
243+
>>> @dataclass
244+
... class User:
245+
... name: str
246+
...
247+
248+
Imagine using f-strings in to generate HTML for the ``User``:
249+
250+
.. testsetup::
251+
252+
class User:
253+
name: str
254+
def __init__(self, name: str):
255+
self.name = name
256+
257+
258+
.. doctest::
259+
260+
>>> # Warning: this is dangerous code. Don't do this!
261+
>>> def user_html(user: User) -> str:
262+
... return f"<div><h1>{user.name}</h1></div>"
263+
...
264+
265+
This code is dangerous because our website lets users type in their own names.
266+
If a user types in a name like ``"<script>alert('evil');</script>"``, the
267+
browser will execute that script when someone else visits their profile page.
268+
This is called a *cross-site scripting (XSS) vulnerability*, and it is a form
269+
of *injection vulnerability*. Injection vulnerabilities occur when user input
270+
is included in a program without proper sanitization, allowing malicious code
271+
to be executed. The same sorts of vulnerabilities can occur when user input is
272+
included in SQL queries, command lines, or other contexts where the input is
273+
interpreted as code.
274+
275+
To prevent this, instead of using f-strings, we can use t-strings. Let's
276+
update our ``user_html()`` function to return a :class:`~string.templatelib.Template`:
277+
278+
>>> from string.templatelib import Template
279+
>>>
280+
>>> def user_html(user: User) -> Template:
281+
... return t"<div><h1>{user.name}</h1></div>"
282+
283+
Now let's implement a function that sanitizes *any* HTML
284+
:class:`~!string.templatelib.Template`:
285+
286+
>>> from html import escape
287+
>>> from string.templatelib import Template
288+
>>>
289+
>>> def sanitize_html_template(template: Template) -> str:
290+
... return ''.join(
291+
... part if isinstance(part, str) else escape(part.value)
292+
... for part in template
293+
... )
294+
...
295+
296+
This function iterates over the parts of the
297+
:class:`~!string.templatelib.Template`, escaping any interpolated values using
298+
the :func:`html.escape` function, which converts special characters like `<`,
299+
`>`, and `&` into their HTML-safe equivalents.
300+
301+
Now we can tie it all together:
302+
303+
.. testsetup::
304+
305+
from dataclasses import dataclass
306+
from string.templatelib import Template
307+
from html import escape
308+
@dataclass
309+
class User:
310+
name: str
311+
def sanitize_html_template(template: Template) -> str:
312+
return ''.join(
313+
part if isinstance(part, str) else escape(part.value)
314+
for part in template
315+
)
316+
def user_html(user: User) -> Template:
317+
return t"<div><h1>{user.name}</h1></div>"
318+
319+
.. doctest::
320+
321+
>>> evil_user = User(name="<script>alert('evil');</script>")
322+
>>> template = user_html(evil_user)
323+
>>> safe = sanitize_html_template(template)
324+
>>> print(safe)
325+
<div><h1>&lt;script&gt;alert(&#x27;evil&#x27;);&lt;/script&gt;</h1></div>
326+
327+
We are no longer vulnerable to XSS attacks because we are escaping the
328+
interpolated values before they are included in the rendered HTML.
329+
330+
Of course, there's no need for code that processes
331+
:class:`~!string.templatelib.Template` instances to be limited to returning a
332+
simple string. For instance, we could imagine defining a more complex ``html()``
333+
function that returns a structured representation of the HTML:
334+
335+
>>> from dataclasses import dataclass
336+
>>> from string.templatelib import Template
337+
>>> from html.parser import HTMLParser
338+
>>>
339+
>>> @dataclass
340+
... class Element:
341+
... tag: str
342+
... attributes: dict[str, str]
343+
... children: list[str | Element]
344+
...
345+
>>> def parse_html(template: Template) -> Element:
346+
... """
347+
... Uses python's built-in HTMLParser to parse the template,
348+
... handle any interpolated values, and return a tree of
349+
... Element instances.
350+
... """
351+
... pass
352+
...
353+
354+
A full implementation of this function would be quite complex and is not
355+
provided here. That said, the fact that it is possible to implement a method
356+
like ``parse_html()`` showcases the flexibility and power of t-strings.
357+
358+
163359
.. _tut-string-format:
164360

165361
The String format() Method

0 commit comments

Comments
 (0)