Python 3.14 shipped with one of the most significant string handling features since f-strings landed in Python 3.6: template string literals, or t-strings. Defined in PEP 750, t-strings look almost identical to f-strings but behave in a fundamentally different way. Instead of eagerly concatenating your string, they produce a Template object that preserves the boundary between static text and interpolated values.
This distinction might sound subtle, but it unlocks an entire class of use cases that f-strings simply cannot handle safely: parameterized SQL queries, auto-escaped HTML output, structured logging, and internationalization. If you write any code that mixes user-supplied data with structured text, t-strings are worth your attention.
In this guide, we will walk through the mechanics of t-strings, explore the Template and Interpolation types, and build practical examples you can apply today.
The Problem with F-Strings
F-strings are fantastic for quick formatting, but they have a fundamental limitation: once the string is evaluated, you cannot distinguish the literal parts from the interpolated parts.
username = "alice"
query = f"SELECT * FROM users WHERE name = '{username}'"
The result is a plain str. There is no way for a downstream function to know that 'alice' was injected dynamically. This makes it impossible for a library to automatically apply escaping, parameterization, or any kind of structural validation after the fact.
T-strings solve this by deferring the final assembly of the string, giving you (or your libraries) a chance to inspect and transform each part before producing the output.
T-String Basics
A t-string uses the t prefix instead of f:
from string.templatelib import Template, Interpolation
name = "World"
greeting = t"Hello, {name}!"
The variable greeting is not a str. It is a Template instance with two key attributes:
strings: a tuple of the static text segments:("Hello, ", "!")interpolations: a tuple ofInterpolationobjects, one per{}expression
Each Interpolation carries rich metadata:
interp = greeting.interpolations[0]
print(interp.value) # "World"
print(interp.expression) # "name"
print(interp.conversion) # None
print(interp.format_spec) # ""
The expression field stores the original source code text inside the braces. The conversion field captures !r, !s, or !a if present. The format_spec stores everything after the colon, such as :.2f.
Processing Templates
The power of t-strings comes from writing functions that consume Template objects. Here is a minimal function that replicates f-string behavior:
def f(template: Template) -> str:
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
else:
value = item.value
if item.conversion == "r":
value = repr(value)
elif item.conversion == "s":
value = str(value)
elif item.conversion == "a":
value = ascii(value)
parts.append(format(value, item.format_spec))
return "".join(parts)
name = "World"
value = 42.0
result = f(t"Hello {name!r}, value: {value:.2f}")
The Template object is iterable. When you iterate over it, you get alternating str segments and Interpolation objects, making it straightforward to process each piece in order.
Practical Use Case: Safe SQL Queries
One of the most compelling applications of t-strings is building parameterized SQL queries. Instead of concatenating user input directly into SQL (a classic injection vulnerability), you can extract the values and pass them as parameters:
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("?")
params.append(item.value)
return "".join(query_parts), params
username = "alice'; DROP TABLE users;--"
query, params = sql(t"SELECT * FROM users WHERE name = {username}")
The result is:
query: "SELECT * FROM users WHERE name = ?"
params: ["alice'; DROP TABLE users;--"]
The malicious input never touches the query structure. It is cleanly separated into the parameters list, ready to be passed to your database driver's parameterized query interface. This is not just convenient syntax sugar. It is a structural guarantee that user data cannot escape into the SQL.
Practical Use Case: Auto-Escaped HTML
Another natural fit is HTML generation with automatic escaping:
from html import escape
def html(template: Template) -> str:
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
else:
parts.append(escape(str(item.value)))
return "".join(parts)
user_input = '<script>alert("xss")</script>'
safe_html = html(t"<div class='message'>{user_input}</div>")
The output:
<div class='message'><script>alert("xss")</script></div>
Every interpolated value gets escaped automatically. The static HTML structure passes through untouched. You get the ergonomics of inline string formatting with the safety of a template engine.
Practical Use Case: Structured Logging
T-strings are also excellent for structured logging where you want both a human-readable message and machine-parseable key-value pairs:
import json
from datetime import datetime, timezone
def structured_log(level: str, template: Template) -> dict:
message_parts = []
fields = {}
for item in template:
if isinstance(item, str):
message_parts.append(item)
else:
fields[item.expression] = item.value
message_parts.append(str(item.value))
return {
"level": level,
"message": "".join(message_parts),
"fields": fields,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
user_id = 42
action = "login"
duration_ms = 123.4
entry = structured_log("INFO", t"User {user_id} performed {action} in {duration_ms}ms")
print(json.dumps(entry, indent=2))
This produces a log entry with both a readable message string and a fields dictionary containing {"user_id": 42, "action": "login", "duration_ms": 123.4}. The field names are extracted directly from the expression text in the t-string, so you get structured data for free without maintaining separate format strings and argument lists.
T-Strings Support Full F-String Syntax
T-strings support everything f-strings support: arbitrary expressions, method calls, format specifications, and conversions.
from decimal import Decimal
price = Decimal("19.99")
quantity = 3
items = ["apple", "banana", "cherry"]
receipt = t"""
Total: {price * quantity:.2f}
Items: {', '.join(items)}
Count: {len(items)}
Debug: {items!r}
"""
Each of these expressions becomes a separate Interpolation with its computed value, original expression text, and any format spec or conversion flag. The Template preserves all of this metadata for your processing function to use however it needs.
Nesting and Composition
Templates can be nested. A t-string inside another t-string produces a nested Template, which your processing functions can handle recursively:
def render(template: Template) -> str:
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
elif isinstance(item.value, Template):
parts.append(render(item.value))
else:
parts.append(str(item.value))
return "".join(parts)
header = t"<h1>{title}</h1>"
page = t"<html><body>{header}<p>{content}</p></body></html>"
This composability makes t-strings viable as the foundation for lightweight DSLs and template systems.
Key Differences from F-Strings at a Glance
| Feature | F-String | T-String |
|---------|----------|----------|
| Prefix | f"..." | t"..." |
| Return type | str | Template |
| Eager evaluation | Yes | Values evaluated, assembly deferred |
| Access to parts | No | Yes, via .strings and .interpolations |
| Security processing | Not possible | Built-in support |
| Format specs / conversions | Supported | Supported (preserved as metadata) |
When to Use T-Strings vs F-Strings
Stick with f-strings for simple, trusted string formatting where you control all the inputs: log messages with known values, display strings in CLI tools, or debug output. They are concise and fast.
Reach for t-strings when any of these apply:
- User-supplied data is being mixed into structured text (SQL, HTML, XML, JSON)
- You want structured logging with automatic field extraction
- You are building a library or framework that processes string templates
- You need internationalization where translators work with template patterns
- You want to validate or transform interpolated values before assembly
Conclusion
T-strings are not a replacement for f-strings. They are a new tool for a different job. Where f-strings optimize for convenience in simple cases, t-strings optimize for safety and flexibility in complex ones. The Template type gives you structural access to both the static and dynamic parts of a formatted string, enabling patterns that were previously impossible without dedicated template engines.
With Python 3.14 now widely available, t-strings are ready for production use. Libraries across the ecosystem are already adding Template-aware APIs, from web frameworks to database drivers to logging libraries. If you are starting a new Python project in 2026, t-strings should be part of your toolkit.