Intermediate
If you have ever built a web application that takes user input and drops it straight into an SQL query or an HTML template, you already know the sinking feeling that comes with discovering an injection vulnerability in production. Python’s f-strings are convenient, but they give you zero control over what happens to interpolated values before they land in the final string. You format it, it is done — no sanitization, no escaping, no second chances.
Python 3.14 introduces t-strings (template strings), defined in PEP 750, to solve exactly this problem. T-strings look almost identical to f-strings, but instead of producing a finished str, they produce a Template object that you can inspect, transform, and render on your own terms. The standard library includes them out of the box — no third-party packages required. You just need Python 3.14 or later.
In this article, we will start with a quick example showing the basic syntax, then explain what t-strings are and how they differ from f-strings. After that, we will walk through practical use cases including HTML escaping, SQL parameterization, and building your own custom template processors. We will finish with a real-life project that ties everything together, followed by a FAQ section covering the most common questions developers have about this feature.
T-Strings in Python: Quick Example
Here is the simplest possible t-string. Notice the t prefix instead of f:
# quick_example.py
from templatelib import Template, Interpolation
name = "World"
greeting = t"Hello, {name}!"
# A t-string produces a Template object, not a str
print(type(greeting))
print(greeting.strings)
print(greeting.interpolations)
# Render it manually
parts = []
for item in greeting:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, Interpolation):
parts.append(str(item.value))
print("".join(parts))
Output:
<class 'templatelib.Template'>
('Hello, ', '!')
(Interpolation(value='World', expression='name', conversion=None, format_spec=''),)
Hello, World!
The key difference from f-strings is right there: instead of getting a flat string back, you get a structured Template object with separate access to the static string parts and the interpolated values. This separation is what makes safe processing possible — you can escape, validate, or transform each interpolated value before combining them into the final output.
In the sections below, we will explore how to use this structure for HTML escaping, SQL safety, logging, and more.

What Are T-Strings and Why Use Them?
T-strings are a new string prefix introduced in Python 3.14 through PEP 750. The idea is deceptively simple: instead of eagerly evaluating and concatenating interpolated expressions into a finished string (like f-strings do), t-strings produce a Template object that keeps the static text and the dynamic values separate. You then decide how to combine them.
Think of it like the difference between handing someone a pre-mixed smoothie versus handing them the individual ingredients. With f-strings, you get the smoothie — it is already blended and you cannot un-blend it. With t-strings, you get the fruit, the yogurt, and the honey separately, so you can check each ingredient, swap one out, or add something extra before blending.
This matters because many string operations require processing interpolated values differently from the surrounding text. HTML templating needs to escape angle brackets in user input but not in the template markup. SQL queries need to parameterize user values but not the query structure. Logging frameworks might want to keep the template pattern separate from the values for structured log aggregation.
| Feature | f-strings | t-strings |
|---|---|---|
| Prefix | f"..." | t"..." |
| Return type | str | Template |
| Eager evaluation | Yes — produces final string immediately | No — produces structured Template object |
| Access to raw values | No | Yes, via .interpolations |
| Custom processing | Not possible | Yes — write your own renderer |
| Injection safe | No | Yes, when used with a safe renderer |
| Expression support | Any Python expression | Any Python expression |
| Format specs | {value:.2f} | {value:.2f} (preserved in Interpolation) |
The bottom line: use f-strings when you just need a quick formatted string for display or debugging. Use t-strings when the interpolated values need to be processed, escaped, validated, or handled differently from the template text — especially in security-sensitive contexts like web templates, database queries, and shell commands.
Anatomy of a Template Object
Before writing custom processors, you need to understand what is inside a Template object. Let us inspect one in detail:
# template_anatomy.py
from templatelib import Template, Interpolation
user = "Alice"
score = 95.7
result = t"Player {user} scored {score:.1f} points"
# The Template has two key attributes
print("strings:", result.strings)
print("interpolations:", result.interpolations)
print()
# Each Interpolation carries metadata
for interp in result.interpolations:
print(f" value: {interp.value!r}")
print(f" expression: {interp.expression!r}")
print(f" conversion: {interp.conversion!r}")
print(f" format_spec: {interp.format_spec!r}")
print()
Output:
strings: ('Player ', ' scored ', ' points')
interpolations: (Interpolation(value='Alice', expression='user', conversion=None, format_spec=''), Interpolation(value=95.7, expression='score', conversion=None, format_spec='.1f'))
value: 'Alice'
expression: 'user'
conversion: None
format_spec: ''
value: 95.7
expression: 'score'
conversion: None
format_spec: '.1f'
The strings tuple always has exactly one more element than interpolations. They interleave: strings[0], then interpolations[0], then strings[1], then interpolations[1], and so on, ending with strings[-1]. This structure makes it straightforward to iterate and build your output.
Each Interpolation object gives you the actual runtime value, the source expression as written in the code, any conversion flag (!r, !s, !a), and the format_spec string. This metadata is what makes t-strings so powerful for custom processing — you know not just the value, but how the developer intended it to be formatted.
Safe HTML Escaping with T-Strings
The most common use case for t-strings is preventing cross-site scripting (XSS) attacks by automatically escaping user input in HTML templates. Here is a reusable HTML renderer:
# html_escape.py
from templatelib import Template, Interpolation
import html
def render_html(template: Template) -> str:
"""Render a t-string with HTML-escaped interpolations."""
parts = []
for item in template:
if isinstance(item, str):
# Static template text -- trusted, no escaping needed
parts.append(item)
elif isinstance(item, Interpolation):
# Dynamic value -- escape to prevent XSS
parts.append(html.escape(str(item.value)))
return "".join(parts)
# Safe usage
username = '<script>alert("hacked")</script>'
safe_html = render_html(t"<div class='greeting'>Welcome, {username}!</div>")
print(safe_html)
Output:
<div class='greeting'>Welcome, <script>alert("hacked")</script>!</div>
The template markup (<div class='greeting'>) passes through untouched because it is part of the static strings tuple — it is trusted code you wrote. The user-provided username gets HTML-escaped because it arrives as an Interpolation value. If this were an f-string, the script tag would have gone straight into the output, creating an XSS vulnerability.

SQL Parameterization with T-Strings
Another critical use case is building SQL queries safely. Instead of string-concatenating user input into queries (the classic SQL injection vector), t-strings let you extract the values as parameters:
# sql_params.py
from templatelib import Template, Interpolation
def prepare_sql(template: Template) -> tuple[str, list]:
"""Convert a t-string into a parameterized SQL query."""
query_parts = []
params = []
for item in template:
if isinstance(item, str):
query_parts.append(item)
elif isinstance(item, Interpolation):
query_parts.append("?") # Parameter placeholder
params.append(item.value)
return "".join(query_parts), params
# Usage
user_id = 42
status = "active'; DROP TABLE users; --"
query, params = prepare_sql(t"SELECT * FROM users WHERE id = {user_id} AND status = {status}")
print("Query: ", query)
print("Params:", params)
Output:
Query: SELECT * FROM users WHERE id = ? AND status = ?
Params: [42, "active'; DROP TABLE users; --"]
The malicious SQL injection attempt in the status variable gets safely separated as a parameter value instead of being interpolated into the query string. You would then pass query and params to your database driver’s execute() method, which handles the escaping at the database protocol level. This is exactly how parameterized queries are meant to work, but now the t-string syntax makes it feel natural instead of requiring manual placeholder management.
Building Custom Template Processors
The real power of t-strings emerges when you write processors tailored to your application. Here are two practical examples.
Structured Logging Processor
Logging frameworks benefit from keeping the message template separate from the values. This enables log aggregation tools to group messages by pattern even when the values differ:
# structured_log.py
from templatelib import Template, Interpolation
import json
from datetime import datetime
def log_structured(level: str, template: Template) -> dict:
"""Create a structured log entry from a t-string."""
# Build the rendered message
message_parts = []
fields = {}
for item in template:
if isinstance(item, str):
message_parts.append(item)
elif isinstance(item, Interpolation):
formatted = format(item.value, item.format_spec) if item.format_spec else str(item.value)
message_parts.append(formatted)
fields[item.expression] = item.value
return {
"timestamp": datetime.now().isoformat(),
"level": level,
"message": "".join(message_parts),
"fields": fields,
"template": "".join(
s if isinstance(s, str) else "{" + s.expression + "}"
for s in template
)
}
# Usage
user = "alice"
action = "login"
duration_ms = 142.5
entry = log_structured("INFO", t"User {user} performed {action} in {duration_ms:.0f}ms")
print(json.dumps(entry, indent=2))
Output:
{
"timestamp": "2026-04-06T10:30:00.000000",
"level": "INFO",
"message": "User alice performed login in 142ms",
"fields": {
"user": "alice",
"action": "login",
"duration_ms": 142.5
},
"template": "User {user} performed {action} in {duration_ms}ms"
}
Notice how the log entry contains both the rendered message for human readability and the raw template pattern plus field values for machine processing. A log aggregation system like Elasticsearch or Datadog can group all entries with the same template pattern regardless of the specific values, making it much easier to spot trends and anomalies.

Shell Command Builder with Escaping
Building shell commands from user input is another injection-prone operation. T-strings make it safe:
# shell_safe.py
from templatelib import Template, Interpolation
import shlex
def safe_command(template: Template) -> str:
"""Build a shell command with properly escaped arguments."""
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, Interpolation):
# Shell-escape any interpolated value
parts.append(shlex.quote(str(item.value)))
return "".join(parts)
# Dangerous user input
filename = 'my file.txt; rm -rf /'
cmd = safe_command(t"cat {filename} | grep 'pattern'")
print(cmd)
Output:
cat 'my file.txt; rm -rf /' | grep 'pattern'
The shlex.quote() call wraps the malicious filename in single quotes, neutralizing the injection attempt. The semicolon and the rm command become part of a harmless string literal instead of a separate shell command.
Nested Templates and Composition
T-strings can be nested — you can interpolate one Template inside another. This is useful for composing complex outputs from smaller reusable pieces:
# nested_templates.py
from templatelib import Template, Interpolation
def render_html(template: Template) -> str:
"""Render with HTML escaping, supporting nested templates."""
import html as html_mod
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, Interpolation):
if isinstance(item.value, Template):
# Recursively render nested templates
parts.append(render_html(item.value))
else:
parts.append(html_mod.escape(str(item.value)))
return "".join(parts)
# Build a page from composable pieces
title = "My Page"
username = '<b>Alice</b>'
header = t"<header><h1>{title}</h1></header>"
body = t"<main>Welcome, {username}</main>"
page = t"<html>{header}{body}</html>"
print(render_html(page))
Output:
<html><header><h1>My Page</h1></header><main>Welcome, <b>Alice</b></main></html>
The nested templates get recursively processed, so the HTML structure from the inner templates passes through as trusted content while user-provided values like username still get escaped. This composability pattern is what makes t-strings viable for real template engines, not just one-off string operations.

Real-Life Example: Safe HTML Email Builder
Let us tie everything together with a practical project — a safe HTML email builder that uses t-strings to prevent injection while keeping the template code clean and readable:
# email_builder.py
from templatelib import Template, Interpolation
import html as html_mod
def render_email(template: Template) -> str:
"""Render an HTML email template with auto-escaping."""
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, Interpolation):
if isinstance(item.value, Template):
parts.append(render_email(item.value))
else:
parts.append(html_mod.escape(str(item.value)))
return "".join(parts)
def build_order_email(customer_name: str, items: list[dict], total: float) -> str:
"""Build an order confirmation email safely."""
# Build item rows from potentially untrusted product names
rows = []
for item in items:
row = t"<tr><td>{item['name']}</td><td>{item['qty']}</td><td>${item['price']:.2f}</td></tr>"
rows.append(render_email(row))
item_rows = "\n".join(rows)
email = t"""<html>
<body style='font-family: Arial, sans-serif;'>
<h2>Order Confirmation</h2>
<p>Hi {customer_name},</p>
<p>Thank you for your order! Here is your summary:</p>
<table border='1' cellpadding='8' cellspacing='0'>
<tr style='background: #333; color: white;'>
<th>Product</th><th>Qty</th><th>Price</th>
</tr>
{item_rows}
</table>
<p><strong>Total: ${total:.2f}</strong></p>
</body>
</html>"""
return render_email(email)
# Test with potentially malicious input
order_items = [
{"name": 'Python Book <script>alert("xss")</script>', "qty": 1, "price": 39.99},
{"name": "USB-C Cable", "qty": 2, "price": 12.50},
{"name": "Mechanical Keyboard", "qty": 1, "price": 89.00},
]
result = build_order_email(
customer_name="Bob <img src=x onerror=alert(1)>",
items=order_items,
total=153.99
)
print(result)
Output:
<html>
<body style='font-family: Arial, sans-serif;'>
<h2>Order Confirmation</h2>
<p>Hi Bob <img src=x onerror=alert(1)>,</p>
<p>Thank you for your order! Here is your summary:</p>
<table border='1' cellpadding='8' cellspacing='0'>
<tr style='background: #333; color: white;'>
<th>Product</th><th>Qty</th><th>Price</th>
</tr>
<tr><td>Python Book <script>alert("xss")</script></td><td>1</td><td>$39.99</td></tr>
<tr><td>USB-C Cable</td><td>2</td><td>$12.50</td></tr>
<tr><td>Mechanical Keyboard</td><td>1</td><td>$89.00</td></tr>
</table>
<p><strong>Total: $153.99</strong></p>
</body>
</html>
Both the customer name and the malicious product name get HTML-escaped automatically, while the table structure and email markup remain intact. This is exactly the kind of “secure by default” behavior that t-strings were designed to provide. You could extend this pattern with a Safe wrapper class for pre-sanitized values that should not be double-escaped.

Frequently Asked Questions
What version of Python do I need to use t-strings?
T-strings require Python 3.14 or later. They were introduced through PEP 750 and are part of the standard library via the templatelib module. If you are on an earlier version, you will need to upgrade. You can check your version by running python --version in your terminal. Python 3.14 was released in October 2025.
How are t-strings different from str.format() and Template strings?
The str.format() method and string.Template both produce finished strings — they do not give you access to the interpolated values before rendering. T-strings produce a Template object that keeps static text and dynamic values separate, letting you process each value individually. This makes t-strings the only built-in option that supports safe, context-aware rendering out of the box.
Are t-strings slower than f-strings?
T-strings have slightly more overhead than f-strings because they create a Template object instead of immediately concatenating a string. However, the difference is negligible for most applications. The extra cost is in the range of microseconds per operation. If you are in a tight loop formatting millions of strings per second and do not need custom processing, stick with f-strings. For everything else, the safety and flexibility of t-strings more than justify the small performance cost.
Can I use t-strings as a drop-in replacement for f-strings?
Not directly, because t-strings return a Template object instead of a str. You need a rendering function to convert the template to a string. However, writing a simple render() function that concatenates all parts without modification gives you f-string-equivalent behavior. The migration path is: change the prefix from f to t, add a render call, then gradually add escaping or processing logic where needed.
Will web frameworks like Django and Flask adopt t-strings?
Several framework maintainers have expressed interest in t-string integration. The Django template engine and Jinja2 (used by Flask) could potentially use t-strings as a lower-level primitive for their template rendering. However, adoption takes time — expect third-party libraries to provide t-string-based template engines before the major frameworks integrate them into their core APIs. In the meantime, you can use t-strings in your own application code alongside existing template engines.
Conclusion
T-strings bring a powerful new capability to Python’s string formatting toolkit. We covered the basic syntax and the Template object anatomy, then built practical processors for HTML escaping, SQL parameterization, structured logging, and shell command safety. The real-life email builder project showed how these patterns combine to create secure-by-default templating in real applications.
The key takeaway is that t-strings do not replace f-strings — they complement them. Use f-strings for quick formatting where safety is not a concern, and use t-strings when interpolated values need processing before they reach the output. The ability to inspect and transform each value individually is what makes the difference between a convenient string and a secure one.
For the complete specification, read PEP 750 and the templatelib documentation.