
Python 3.14.6 shipped June 10, 2026. Most developers noticed the 179 bugfixes. Fewer noticed that t-strings — the feature that changes how you build dynamic strings — are now stable and production-ready. If you are still reaching for f-strings to construct SQL queries, HTML fragments, or log messages with untrusted input, you are introducing risk that a one-character prefix change would eliminate.
What T-Strings Actually Are
T-strings look identical to f-strings. Replace the f prefix with t and the syntax is the same:
name = "Alice"
# F-string: evaluates immediately to a str
greeting = f"Hello, {name}" # "Hello, Alice"
# T-string: returns a Template object
template = t"Hello, {name}" # string.templatelib.Template
The difference is what you get back. An f-string produces a plain str the moment Python executes the line. A t-string returns a Template object from the new string.templatelib module. That object stores the static text and the interpolated values separately, giving you a chance to inspect, transform, or sanitize them before they become a string.
The Template has three key attributes:
.strings— the literal text segments between your variables.interpolations— a tuple ofInterpolationobjects, each carrying.value,.expression,.conversion, and.format_spec.values— shorthand tuple of just the interpolated values
PEP 750, which introduced t-strings, was co-authored by Guido van Rossum himself. That is not a coincidence — this is not a committee feature bolted on at the margins. It is a deliberate language-level solution to problems that f-strings structurally cannot solve.
Use Case 1: SQL Injection Prevention
The classic f-string SQL pattern is a well-known vulnerability:
# This is dangerous — please stop writing this
username = request.form["username"]
query = f"SELECT * FROM users WHERE name = '{username}'"
db.execute(query)
# If username = "'; DROP TABLE users; --" → you've lost the table
The correct fix has always been parameterized queries, but they require a different API: pass the SQL string and values separately. T-strings make the safe path the natural path:
from sql_tstring import sql
username = request.form["username"]
query, params = sql(t"SELECT * FROM users WHERE name = {username}")
# query = "SELECT * FROM users WHERE name = ?"
# params = ("Alice",) — safely separated
db.execute(query, params)
The sql-tstring library receives the Template object, sees the interpolated value, and generates the parameterized form automatically. You write natural Python. The library handles the rest. If you use psycopg for PostgreSQL, the 3.3 release ships native t-string support — pass t-strings directly to conn.execute() with no adapter needed.
Use Case 2: HTML Escaping Without Remembering to Escape
XSS bugs come from forgotten html.escape() calls. T-strings let you build an html() processor that escapes interpolated values automatically:
from html import escape
from string.templatelib import Template
def html(template: Template) -> str:
result = []
strings = iter(template.strings)
result.append(next(strings))
for interp in template.interpolations:
result.append(escape(str(interp.value)))
result.append(next(strings))
return "".join(result)
user_input = "<script>alert('xss')</script>"
safe = html(t"<p>Welcome, {user_input}</p>")
# <p>Welcome, <script>alert('xss')</script></p>
Write this function once, use it everywhere. No linting rule required. The escaping is structural, not optional. For pre-built options, awesome-t-strings catalogs the ecosystem: tstring-html, ludic, and pyhtml-enhanced all accept Template objects.
Use Case 3: Structured Logging
Log messages as plain strings lose their structure the moment they are rendered. T-strings let you build log entries that carry field metadata:
def log_structured(template: Template) -> dict:
fields = {i.expression: i.value for i in template.interpolations}
message = "".join(
s + str(i.value)
for s, i in zip(template.strings, template.interpolations)
) + template.strings[-1]
return {"message": message, "fields": fields}
user_id = 12345
action = "password_reset"
entry = log_structured(t"User {user_id} triggered {action}")
# {"message": "User 12345 triggered password_reset",
# "fields": {"user_id": 12345, "action": "password_reset"}}
The .expression attribute is particularly useful for PII detection: inspect field names at log time and automatically redact anything named email, password, or ssn before the entry hits your log aggregator.
F-Strings Are Not Going Anywhere — Here Is When to Use Each
The criticism that Python now has five string formatting approaches is technically accurate and mostly irrelevant. The decision is simpler than it looks:
- Use f-strings when the string is for display, debugging, or consumption by something that treats it as opaque text. All values are trusted. You just need a string.
- Use t-strings when the string will be consumed by a system that interprets it — a database, a browser, a log aggregator, an LLM. Any untrusted input. Any output where structure matters.
The rule of thumb: if your string goes into a system that parses it, use t-strings.
Where the Ecosystem Stands
T-strings require Python 3.14. That is the honest constraint. If you are on 3.12 or 3.13, you are waiting. For greenfield projects and services you control, 3.14 is stable and Python 3.14.6 is the current patch.
Ecosystem adoption is ahead of where you might expect. psycopg 3.3 ships t-string support today. Django ORM t-string integration is under active discussion. The Python 3.14 release notes cover the full feature set, and Real Python’s t-strings guide is the best deep-dive if you want to build your own processors.
T-strings will not replace f-strings. They will become the standard for any Python code that touches untrusted data. The change is small — one character prefix — but the category of bugs it eliminates is not.













