Warm-up · Activity 1 of 7
Warm-up from module A4 and the f-string lesson: what does this print?
from decimal import Decimal
price = Decimal("0.1") + Decimal("0.2")
print(f"{price} {0.1 + 0.2:.2f}")// A5.1 · ~30 min · Advanced
After this lesson you can read a t-string as a Template of static strings and interpolations, write a renderer that applies conversions and format specs, and escape untrusted values where an f-string would have mixed them in.
Lesson 1 of 5 in A5 Modern Python and safe code
You will be able to
Warm-up · Activity 1 of 7
from decimal import Decimal
price = Decimal("0.1") + Decimal("0.2")
print(f"{price} {0.1 + 0.2:.2f}")Predict · Activity 2 of 7
name = "Ada"
greeting = t"Hello {name}!"
print(type(greeting).__name__, greeting.strings)Practice · Activity 3 of 7
from string.templatelib import Interpolation
def shout(template):
parts = []
for part in template:
if isinstance(part, ____):
parts.append(str(part.value).upper())
else:
parts.append(part)
return "".join(parts)Practice · Activity 4 of 7
x = "hi"
tpl = t"{x!r:>6}"
i = tpl.interpolations[0]
print(i.value, i.conversion, i.format_spec)Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
name = "Ada"
greeting = t"Hi {name}!"
print(greeting == "Hi Ada!", "".join(greeting.strings))Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
render_html() walks a t-string. It applies each conversion and format spec as an f-string would, then escapes the result with html.escape, and copies the static markup unchanged. The user name contains angle brackets, the kind of text an attacker would try. The last line shows the f-string version for contrast: once it is a str, the markup the programmer wrote and the text the user typed can no longer be told apart.
main.py
import html
from string.templatelib import Interpolation, Template, convert
def render_html(template: Template) -> str:
"""Join a template, escaping every interpolated value for HTML."""
parts: list[str] = []
for part in template:
if isinstance(part, Interpolation):
value = convert(part.value, part.conversion)
text = format(value, part.format_spec)
parts.append(html.escape(text))
else:
parts.append(part) # static text, written by the programmer
return "".join(parts)
user = "Ada <admin>"
total = 1234.5
page = t"<p>Hello {user}, you owe {total:,.2f} EUR</p>"
print(page.strings)
print(page.values)
print(render_html(page))
print(f"<p>Hello {user}</p>") # an f-string has already merged the parts
Run it with
python main.pyOutput
('<p>Hello ', ', you owe ', ' EUR</p>')
('Ada <admin>', 1234.5)
<p>Hello Ada <admin>, you owe 1,234.50 EUR</p>
<p>Hello Ada <admin></p>Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercise 1 of 2
Complete render(template) in main.py so that it returns exactly what the matching f-string would give. The starter inserts str(value) and ignores the conversion and the format spec. Apply part.conversion with convert() and then part.format_spec with format(). The tests compare with f-strings, including the = specifier.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
convert(part.value, part.conversion) returns the value unchanged when conversion is None, and str(), repr() or ascii() of it otherwise.
format(value, "") is the same as str(value), so format(converted, part.format_spec) works when there is no spec too.
The = specifier needs no code of its own: the t-string already puts "n=" into strings and sets conversion to "r".
One way to solve it. Yours can look different and still pass the checks.
from string.templatelib import Interpolation, Template, convert
def render(template: Template) -> str:
"""Render a template exactly like the matching f-string."""
out: list[str] = []
for part in template:
if isinstance(part, Interpolation):
value = convert(part.value, part.conversion)
out.append(format(value, part.format_spec))
else:
out.append(part)
return "".join(out)
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from string.templatelib import Interpolation, Template, convert
def render(template: Template) -> str:
"""Render a template exactly like the matching f-string."""
out: list[str] = []
for part in template:
if isinstance(part, Interpolation):
# Apply part.conversion (with convert) and part.format_spec (with format).
out.append(str(part.value))
else:
out.append(part)
return "".join(out)
test_main.py
from main import render
def test_plain():
"""A plain value is inserted as it is"""
name = "Ada"
got = render(t"Hi {name}!")
assert got == "Hi Ada!", f"render gave {got!r}, expected 'Hi Ada!'"
def test_format_spec():
"""The format spec is applied, as in an f-string"""
price = 3.14159
got = render(t"{price:.2f} EUR")
assert got == "3.14 EUR", f"render gave {got!r}: apply part.format_spec with format()"
def test_conversion():
"""!r is applied, as in an f-string"""
word = "hi"
got = render(t"say {word!r}")
assert got == "say 'hi'", f"render gave {got!r}: apply part.conversion with convert()"
def test_same_as_fstring():
"""The = specifier and a width give what the f-string gives"""
n = 5
got = render(t"{n=} [{n:>4}]")
want = f"{n=} [{n:>4}]"
assert got == want, f"render gave {got!r}, the f-string gives {want!r}"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
Complete safe_html(template). Escape every interpolated value with html.escape, after applying its conversion and format spec, and keep the static markup unchanged. If the argument is not a Template, for example the str from an f-string, raise TypeError: a renderer that accepted a str could not know which parts to escape.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Start with if not isinstance(template, Template): raise TypeError(...).
Loop over the template. For an Interpolation, format it as in the previous exercise and append html.escape(text); append a str part unchanged.
html.escape escapes <, >, & and, by default, both kinds of quotes.
One way to solve it. Yours can look different and still pass the checks.
import html
from string.templatelib import Interpolation, Template, convert
def safe_html(template: Template) -> str:
"""Build HTML from a t-string, escaping every interpolated value."""
if not isinstance(template, Template):
raise TypeError("safe_html needs a t-string, not a str")
out: list[str] = []
for part in template:
if isinstance(part, Interpolation):
text = format(convert(part.value, part.conversion), part.format_spec)
out.append(html.escape(text))
else:
out.append(part)
return "".join(out)
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import html
from string.templatelib import Interpolation, Template, convert
def safe_html(template: Template) -> str:
"""Build HTML from a t-string, escaping every interpolated value."""
# This escapes the static markup too, ignores the format specs,
# and quietly accepts a plain str.
text = "".join(str(part.value) if isinstance(part, Interpolation) else part for part in template)
return html.escape(text)
test_main.py
from main import safe_html
def test_escapes_values():
"""Markup in a value is escaped"""
name = "<b>Ada</b>"
got = safe_html(t"<p>{name}</p>")
assert got == "<p><b>Ada</b></p>", f"safe_html gave {got!r}"
def test_keeps_static_markup():
"""The markup written in the t-string itself stays markup"""
city = "Bonn"
got = safe_html(t'<a href="/map">{city}</a>')
assert got == '<a href="/map">Bonn</a>', f"safe_html gave {got!r}: escape only the interpolations"
def test_quotes_escaped():
"""Quotes in a value are escaped too"""
title = 'say "hi"'
got = safe_html(t"<b>{title}</b>")
assert got == "<b>say "hi"</b>", f"safe_html gave {got!r}"
def test_format_spec():
"""The format spec is applied before escaping"""
total = 1234.5
got = safe_html(t"<td>{total:,.2f}</td>")
assert got == "<td>1,234.50</td>", f"safe_html gave {got!r}"
def test_rejects_str():
"""A plain str (an f-string result) raises TypeError"""
name = "<b>Ada</b>"
try:
safe_html(f"<p>{name}</p>")
except TypeError:
pass
else:
assert False, "safe_html accepted a str; raise TypeError unless it is a Template"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyname = "Ada"
greeting = t"Hi {name}" + "!"
What Python prints
TypeError: can only concatenate string.templatelib.Template (not "str") to string.templatelib.TemplateWhy, and the fix
Python cannot know whether "!" should be static text or a value, so Template + str is refused. Put the text inside the t-string, t"Hi {name}!", or wrap it: + Template("!") for static text, or + Template(Interpolation(value, "name")) for a value.
name = "Ada"
print(t"Hi {name}".upper())
What Python prints
AttributeError: 'string.templatelib.Template' object has no attribute 'upper'Why, and the fix
A Template has no string methods, because it is not text yet. Render it first, with your own renderer, and call str methods on the result, or change the values before they go into the t-string.
name = "ada"
tpl = t"{name:upper}"
part = tpl.interpolations[0]
print(format(part.value, part.format_spec))
What Python prints
ValueError: Invalid format specifier 'upper' for object of type 'str'Why, and the fix
A t-string accepts any text after the colon, because the renderer decides what it means. format() only understands the standard mini-language. If your renderer invents specs such as upper, handle them itself before falling back to format().
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.