Every Python developer reaches for an f-string by reflex. They’re fast, readable, and built into the language. But f-strings have a structural flaw you cannot patch: they evaluate immediately and hand you a plain string. Once that string exists, the boundary between your static template text and user-supplied data is gone. That is the root cause of f-string-built SQL injection vulnerabilities and XSS bugs — not careless developers, but a language feature that cannot preserve structure. Python 3.14 ships t-strings (template string literals, defined in PEP 750) to fix exactly this. They use the same familiar syntax but return a Template object instead of a string, giving you the structure you need to make injection-safe processing possible at the language level.
Lazy vs Eager: The Core Difference
F-strings are eager. f"Hello {name}" evaluates name immediately and returns a str. T-strings are lazy. t"Hello {name}" evaluates the expression but returns a Template object from the new string.templatelib module. That object contains two things: the static parts and the interpolations.
from string.templatelib import Template, Interpolation
name = "Alice"
# F-string: returns str immediately
greeting_f = f"Hello {name}"
print(type(greeting_f)) # <class 'str'>
# T-string: returns Template object
greeting_t = t"Hello {name}"
print(type(greeting_t)) # <class 'string.templatelib.Template'>
# Inspect the structure
print(list(greeting_t.strings)) # ['Hello ', '']
print(greeting_t.interpolations[0].value) # 'Alice'
print(greeting_t.interpolations[0].expression) # 'name'
The Interpolation object carries the evaluated value, the original expression string, any conversion flag (!r, !s, !a), and the format spec. This is the metadata that makes safe processing possible. You know what value came from where, and you decide what to do with it before any output is produced.
Safe SQL: The Use Case That Justifies Everything
Building SQL queries with f-strings is one of the most persistent security mistakes in Python codebases. It reads naturally and it is dangerous: a malicious input like '; DROP TABLE users; -- becomes part of your query string with no escape. T-strings let you build a thin sql() helper that produces a parameterized query directly from the template’s structure:
from string.templatelib import Template
def sql(template: Template) -> tuple[str, list]:
query_parts = []
params = []
for item in template:
if isinstance(item, str):
query_parts.append(item)
else:
query_parts.append("?") # placeholder, never the value
params.append(item.value)
return "".join(query_parts), params
# Write it like an f-string — safety comes from the processor
user_input = "Alice'; DROP TABLE users; --"
query, params = sql(t"SELECT * FROM users WHERE name = {user_input}")
cursor.execute(query, params)
# Executes: SELECT * FROM users WHERE name = ?
# Params: ["Alice'; DROP TABLE users; --"]
# The DB driver handles escaping. Injection is structurally impossible.
The dangerous f-string equivalent would have embedded the payload directly into the query string. The t-string version is equally readable to write but architecturally safe. The template structure makes it impossible to accidentally inline user data — the only way to get a value into the query is through the params list that goes through the database driver.
HTML Escaping in Ten Lines
The same pattern applies to HTML generation. Instead of pulling in Jinja2 every time you need to safely embed user content in markup, you can write a safe_html() function using t-strings:
import html
from string.templatelib import Template
def safe_html(template: Template) -> str:
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
else:
parts.append(html.escape(str(item.value)))
return "".join(parts)
user_comment = "<script>alert('xss')</script>"
output = safe_html(t"<div class='comment'>{user_comment}</div>")
# <div class='comment'><script>alert('xss')</script></div>
The XSS payload renders as plain text. This is auto-escaping at the language level without a framework dependency. Libraries like tdom — created by PEP 750 co-author Dave Peck — are already building full HTML templating systems on exactly this foundation.
When f-Strings Still Win
T-strings add a processing step. That overhead is only worth it when you are crossing a security boundary — when user input, external data, or untrusted content is being embedded in a structured output like SQL, HTML, shell commands, or structured log formats. For everyday display strings where you control all the values, f-strings are cleaner and more direct. The rule: if no external data crosses the template boundary, use an f-string. print(f"Found {count} results in {elapsed:.2f}s") does not need the ceremony of a Template object.
The Ecosystem Is Just Getting Started
T-strings are a language primitive, not a library. The real payoff comes when framework authors integrate them. The Django forum has an active thread on adding t-string support to the ORM and template backend. JavaScript developers migrating to Python will recognize the pattern immediately — tagged template literals in JS serve the same purpose. For a deeper look at the string.templatelib API, the Real Python guide and Python Morsels are solid next reads.
Python 3.14.5 is the current release as of May 2026. If you are already on 3.14, open a REPL, import string.templatelib, and start with the SQL example above. The syntax is identical to what you already write — swap f for t, add a five-line processor, and the injection path closes permanently.













