We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modern Syntax and Modernisation step 17 of 18
Template strings (t-strings) and interpolation safety
An f-string hands the renderer a finished string. By then the information that mattered — which characters came from the developer and which came from a user — has been destroyed. That loss is the mechanism behind SQL injection and stored XSS, and no amount of escaping-at-the-call-site fixes it reliably, because the call site is exactly where people forget.
PEP 750 (Python 3.14) adds a second literal form:
name = "<script>alert(1)</script>"
template = t"<li>{name}</li>"
template is a string.templatelib.Template, not a str. It has:
-
.strings— the static chunks, always one longer than the interpolations; -
.interpolations— anInterpolationper replacement field, carrying.value(the evaluated object),.expression(its source text),.conversion("r","s","a"orNone) and.format_spec; - iteration, which yields the two kinds interleaved in source order, skipping empty static chunks.
t-strings escape nothing by themselves. They are not a sanitiser. They are a structure-preserving literal: the renderer you write can finally see the seam.
What you are building
def render_html(template: Template) -> str
def sql(template: Template) -> tuple[str, list[object]]
render_html walks the template, emits static chunks verbatim, and
HTML-escapes every interpolated value — after honouring its conversion and
format spec, in that order.
sql walks the template, emits static chunks verbatim, replaces each
interpolation with a literal ?, and collects the values into a parameter
list. That is a parameterised query: the driver never sees user data as SQL
text.
Then:
def solve(name: str, comment: str, min_age: int, score: float) -> dict[str, object]
which must build three templates as literals inside the function and return
-
"html"—render_html(t"<li><b>{name}</b> said: {comment}</li>") -
"detail"—render_html(t"{name!r} scored {score:.2f}") -
"sql"and"params"— fromsql(t"SELECT * FROM users WHERE name = {name} AND age > {min_age}")
The asymmetry in the first one is the whole lesson: the <b> you typed is
markup and survives; a <b> that arrives in name is data and comes out as
<b>.
The type is the safety
Template is not a str subclass, and this is deliberate. Under
mypy --strict, "prefix " + template is an error, f"{template}" gives you
a Template repr rather than the text, and a function declared
def query(sql: Template) cannot be handed an f-string by accident. The
static gate now enforces at the boundary what a code review used to have to
catch by eye.
Compare with the pre-3.14 tool for the same job, typing.LiteralString
(PEP 675): def query(sql: LiteralString) rejects any string built from
non-literal parts. That protects the static half — but it also rejects
legitimate dynamic query construction. t-strings admit the dynamic parts and
keep them labelled.
Version wall: t"..." is syntax. On 3.13 it is a SyntaxError at import,
so a module using t-strings is a hard 3.14 dependency — declare it in
requires-python, do not try to guard it at runtime.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.