Keep structure available for inspection

An f-string produces a final string with values already incorporated. Introduced in Python 3.14, a t-string produces a Template that retains static strings and interpolated values separately. That separation enables application-defined processing. It does not automatically make every output context safe.

A small teaching example inserts a user name into HTML text. The processor preserves trusted literal structure and escapes dynamic values with html.escape. Its scope is HTML text nodes only. Attributes, URLs, JavaScript and CSS need different treatment; do not reuse this helper there without a separately reviewed contract.

Process values at a defined output boundary

A Template is structured input, not a complete rendering engine. Interpolation expressions still execute as Python expressions, so avoid hidden side effects. Never use eval to turn user input into a template. For SQL, use actual driver parameters rather than assuming textual escaping provides equivalent protection.

The helper rejects conversions and format specifications instead of silently implementing an incomplete policy. Supporting them requires decisions about formatting order, type handling and escaping. If a team shares a processor, document allowed contexts and failure behavior explicitly.

Test ordinary names, markup characters, quotation marks and Persian text. Verify both the returned string and its intended text-node rendering. A format-specification input should fail explicitly. The goal is predictable behavior at the stated boundary, not merely attractive output for a friendly input.

Code example and verification

The notes state the prerequisites and expected outcome. Run the example in a test environment, then adapt it to your project’s data and constraints.

1. Python 3.14+; educational HTML text-node processor

from html import escape
from string.templatelib import Template

def render_html_text(template: Template) -> str:
    output = []
    for index, interpolation in enumerate(template.interpolations):
        output.append(template.strings[index])
        if interpolation.conversion or interpolation.format_spec:
            raise ValueError("formatting is not supported")
        output.append(escape(str(interpolation.value)))
    output.append(template.strings[-1])
    return "".join(output)

name = "<student>"
print(render_html_text(t"<p>Hello {name}</p>"))

Expect <p>Hello &lt;student&gt;</p>. Literal markup must be developer-controlled. This is not a general HTML sanitizer and cannot run on Python 3.12.

2. Inspect literals, values and interpolation metadata

Inspect the structure before writing a processor. expression contains the source expression while value contains its already evaluated result. Retaining expression text does not imply that it should be evaluated again.

# Python 3.14+
name = "لیان"
template = t"Hello {name}!"
assert template.strings == ("Hello ", "!")
assert template.values == ("لیان",)
item = template.interpolations[0]
print("expression:", item.expression)
print("value:", item.value)
print("conversion:", item.conversion)
print("format:", item.format_spec)
assert item.expression == "name"

Expect expression: name and the Persian value. Metadata can support inspection tools, but never pass expression to eval. Side-effecting expressions run during Template creation and should not be repeated by the processor.

3. Compile a Template into bound SQL parameters

This processor binds SQLite values. Literal SQL must remain developer-controlled; identifiers and sort directions are not value parameters and require separate allowlists.

# Python 3.14+; fixed developer-controlled SQL structure only.
import sqlite3

def bind_sql(template):
    pieces, values = [], []
    for index, item in enumerate(template.interpolations):
        pieces.append(template.strings[index])
        if item.conversion or item.format_spec:
            raise ValueError("SQL formatting is not supported")
        pieces.append("?")
        values.append(item.value)
    pieces.append(template.strings[-1])
    return "".join(pieces), values

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE students(id INTEGER,name TEXT)")
db.execute("INSERT INTO students VALUES(?,?)", (1, "Alice"))
user_name = "Robert'); DROP TABLE students;--"
sql, params = bind_sql(t"SELECT id FROM students WHERE name = {user_name}")
assert db.execute(sql, params).fetchall() == []
assert db.execute("SELECT count(*) FROM students").fetchone()[0] == 1
print("Query parameters preserved; table still exists")
db.close()

Expect Query parameters preserved; table still exists. The hostile input remains a parameter. Do not quote {user_name} in the template; the driver binds the placeholder. This helper does not implement arbitrary SQL dialects or template structures.

4. Test HTML text escaping and reject unsupported formatting

Save the introductory processor as safe_html.py and these tests as test_safe_html.py. Both files require Python 3.14+ parsing; installing pytest on an older interpreter does not enable the syntax.

# Save the first example's processor as safe_html.py.
# test_safe_html.py; run with Python 3.14+ and pytest.
import pytest
from safe_html import render_html_text

@pytest.mark.parametrize("value, expected", [
    ("<student>", "<p>&lt;student&gt;</p>"),
    ("A&B", "<p>A&amp;B</p>"),
    ("لیان", "<p>لیان</p>"),
])
def test_text_node_context(value, expected):
    assert render_html_text(t"<p>{value}</p>") == expected

def test_rejects_format_spec():
    value = 42
    with pytest.raises(ValueError):
        render_html_text(t"<p>{value:04d}</p>")

def test_rejects_conversion():
    value = "hello"
    with pytest.raises(ValueError):
        render_html_text(t"<p>{value!r}</p>")

Run python -m pytest -q test_safe_html.py and expect 5 passed. Passing text-node tests does not establish safety for URLs, attributes, CSS or JavaScript; those contexts need their own processing and tests.

Adopt the capability with a narrow contract

Older Python versions cannot parse t-string syntax, even behind a runtime conditional. Declare the required interpreter in builds and deployments. Start with a small non-sensitive path and evaluate whether keeping structure separate actually improves review and testing.

Implementation checklist

  • Require Python 3.14 or newer.
  • Limit this helper to HTML text nodes.
  • Reject unsupported formatting and conversions.
  • Never construct templates through eval of user input.

References

Editorial guidance: Liyan Knowledge.