Python 3.14 shipped eight months ago and most developers are still writing f"SELECT * FROM users WHERE id = {user_id}". That line is a SQL injection waiting to happen, and the f-string is why. T-strings, introduced in Python 3.14 via PEP 750, give you an interception point between the expression and the output. If you touch SQL, HTML, or any user-supplied input in Python, this changes what “safe by default” means.
What T-Strings Are (and Are Not)
T-strings look identical to f-strings. The only difference is the prefix:
name = "Alice"
f_result = f"Hello {name}" # → "Hello Alice" (a str)
t_result = t"Hello {name}" # → Template object (not a str)
That Template object preserves the structure of your string — the static parts and the interpolated values stay separate. You can inspect them:
from string import templatelib
template = t"SELECT * FROM users WHERE id = {user_id} AND active = {is_active}"
# template.strings → ('SELECT * FROM users WHERE id = ', ' AND active = ', '')
# template.interpolations → (Interpolation(user_id, ...), Interpolation(is_active, ...))
Each Interpolation carries .value (the evaluated expression), .expr (the source text), and .format_spec for any formatting instructions. You iterate the template to get alternating strings and interpolations. This is the same design as JavaScript’s tagged template literals, which have been in ES2015 for over a decade. Python is catching up.
The SQL Injection Problem, Solved Structurally
Here is the pattern that kills an entire class of bug:
def sql(template) -> tuple[str, list]:
"""Convert a t-string template to a parameterized query."""
parts, values = [], []
for item in template:
if isinstance(item, str):
parts.append(item)
else: # Interpolation
parts.append("?")
values.append(item.value)
return "".join(parts), values
# Usage
user_id = request.args.get("id") # untrusted input
query, params = sql(t"SELECT * FROM users WHERE id = {user_id}")
conn.execute(query, params)
Even if user_id is "1 OR 1=1", the SQL driver receives ? as the placeholder and ["1 OR 1=1"] as a bound parameter. The malicious string never touches the query structure. With f-strings, you have no hook — the interpolation is already done by the time you have a string.
This is not a hypothetical improvement. Python codebases use f-strings for SQL construction constantly, especially in internal tools, data pipelines, and ORMs that expose raw queries. T-strings make the safe path as convenient as the dangerous one.
HTML Escaping With tdom
The same structural protection applies to HTML. The tdom library (pip install tdom) implements an HTML processor built on t-strings:
import tdom
user_comment = "<script>alert('xss')</script>"
# f-string — the script tag renders in the browser
html_unsafe = f"<p>{user_comment}</p>"
# t-string with tdom — auto-escaped, XSS blocked
html_safe = tdom.html(t"<p>{user_comment}</p>")
# → "<p><script>alert('xss')</script></p>"
The user value is escaped automatically before it reaches the output string. You get f-string-level ergonomics with sanitization baked in.
The Processor Pattern and Ecosystem
PEP 750 deliberately ships no default processors. The spec defines the syntax and the Template structure; what you do with a template is your call. Critics call this a half-finished feature — they have a point. New developers see a t-string and immediately ask “how do I turn this into a string?” The processor pattern is about ten lines of code, but it is still an extra step.
The ecosystem is filling the gap. Libraries available today:
- tdom — HTML templating with automatic XSS escaping
- t-sql — SQL parameterization via t-strings
- tstr — rendering utilities, HTML escaping, SQL, and logging helpers
- tstringlogger — lazy log messages that skip evaluation when the log level is off
The awesome-t-strings repository tracks the growing list. Django integration is in active discussion in the Django forum but has not merged into core yet.
F-String vs T-String at a Glance
| f-string | t-string | |
|---|---|---|
| Returns | str (immediately) | Template object |
| SQL injection safe | No | Yes (with processor) |
| XSS safe | No | Yes (with tdom) |
| Lazy evaluation | No | Yes |
| Built-in processors | N/A | None (by design) |
| Available since | Python 3.6 | Python 3.14 |
When to Use T-Strings (and When Not To)
Use t-strings when you are constructing strings from untrusted input and the destination has an injection surface: SQL queries, HTML templates, shell commands, LLM prompts. The structural separation is worth the extra import and processor setup.
Skip t-strings for general string formatting. If you are building a log line from trusted internal data, an f-string is fine. T-strings add a dependency — you need a processor — and that overhead is not justified when there is no injection risk. The f-string is not going away; it is just the wrong tool for security-sensitive cases.
The upgrade path is straightforward. Python 3.14 is available in all major distributions. Check your current version, install 3.14 if needed, add tdom or a SQL processor to your dependencies, and migrate the query construction and HTML rendering code that currently uses f-strings on untrusted input. That is where the risk lives, and that is where t-strings pay off. The Red Hat developer guide covers the full 3.14 feature set if you want context on what else shipped alongside t-strings.


