SecurityProgramming LanguagesPython

Python t-Strings: Stop Using f-Strings for SQL and HTML

Python 3.14.6 landed on June 10 — a quiet maintenance release with 179 bugfixes. Buried in that version is a feature the security community has wanted since f-strings shipped in 2016: a way to intercept string interpolation before it becomes a vulnerability. Template strings, or t-strings (PEP 750), are now in a stable, production-ready Python release. The ecosystem has caught up. There’s no good reason to wait.

The Problem With f-Strings Nobody Talks About

F-strings are genuinely great. They’re readable, fast, and expressive. For displaying data, logging controlled values, building output for your own code — they’re the right tool. The problem is that Python developers have been using them everywhere, including places where they’re quietly dangerous.

user_id = request.args.get("id")
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)

If user_id is 1 OR 1=1, your database returns every row. If it’s 1; DROP TABLE users; --, you’ve had a much worse day. The f-string evaluates immediately to a plain string — the database never gets a chance to distinguish structure from data. SQL injection via string formatting is consistently one of the most common CVE categories in Python web applications.

The same problem hits HTML generation: an f-string like f"<p>Welcome, {username}!</p>" with a crafted username becomes an XSS vector. Logging with f"Login attempt by {username} from {ip}" lets attackers inject fake log entries via newlines in the username. The root cause is the same in all three cases: f-strings collapse structure and data into a single string before anything can inspect them. t-strings don’t.

How t-Strings Work

A t-string looks almost identical to an f-string — just with a t prefix:

name = "Alice"
result = t"Hello, {name}!"
print(type(result))  # <class 'string.templatelib.Template'>

That Template object holds two things: the static literal chunks and the interpolated values — evaluated, but stored separately. Critically, Template does not implement __str__(). You can’t accidentally render a t-string as a plain string. A library must explicitly process it. That’s the design: the library sees the data before it becomes part of the string, and decides whether to escape it, parameterize it, sanitize it, or reject it.

Three Places to Use t-Strings Right Now

1. SQL Queries (psycopg 3.3)

psycopg 3.3 ships native t-string support. Pass a t-string to cursor.execute() and psycopg automatically converts interpolated values to SQL parameters:

import psycopg

conn = psycopg.connect(dsn)
user_id = request.args.get("id")

with conn.cursor() as cur:
    cur.execute(t"SELECT * FROM users WHERE id = {user_id}")
    rows = cur.fetchall()

psycopg extracts the value, builds a parameterized query (SELECT * FROM users WHERE id = $1), and sends the value as a bound parameter. The SQL injection vector is gone. The code reads almost identically to the dangerous f-string version — but now the library handles parameterization automatically.

2. HTML Escaping

For HTML, write a simple processor or use tdom:

import html
from string.templatelib import Template, Interpolation

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)

username = "<script>alert('xss')</script>"
output = safe_html(t"<p>Welcome, {username}!</p>")
# <p>Welcome, &lt;script&gt;alert('xss')&lt;/script&gt;!</p>

The interpolated value is escaped; the surrounding HTML structure passes through untouched. This separation is exactly what raw string concatenation makes impossible.

3. Safe Logging (Loguru)

Loguru ships t-string support out of the box:

from loguru import logger

username = request.form.get("username")
ip = request.remote_addr
logger.info(t"Login attempt by {username} from {ip}")

Loguru processes the Template, sanitizing control characters and newlines in interpolated values before writing the log entry. Log injection via crafted usernames becomes significantly harder.

When to Keep Using f-Strings

t-strings are not a replacement for f-strings. The decision rule is simple: if the data flowing into your string comes from outside your codebase — user input, a database result, an API response — use a t-string with an appropriate processor. If you control the data entirely — debug output, internal formatting, values your own code created — f-strings are fine and faster.

Don’t rewrite your entire codebase. Identify the high-risk patterns: f-strings adjacent to database calls, HTML generation from user data, log calls that include external input. Grep for f" near cursor.execute, HTML rendering calls, and logger calls. Those are your migration targets.

The Ecosystem Is Ready

The “wait for ecosystem support” argument no longer holds. psycopg 3.3 has native t-string query support. tdom handles HTML. Loguru handles logging. Pyright 1.1.402 has first-class PEP 750 type analysis, so your editor will catch misuse statically. Django’s core team is actively discussing t-string integration for format_html(). The awesome-t-strings community repo catalogs the growing library ecosystem.

Python gave developers a tool — f-strings — that made one class of security bug easier to write. PEP 750 and Python 3.14 close that design gap. Nine years is a long time to wait, but the fix is production-ready. Time to use it.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:Security