tech, developers, and the code underneath

issue 020· news·

PEP 750 lands: Python gets template strings

A new t prefix produces a Template object instead of a string. It's f-strings without the injection vulnerability.

PEP 750 has been accepted for Python 3.14. It adds a t string prefix that produces a Template object rather than a str, giving library authors access to the interpolated values before they are stringified.

This is a small feature with a large security implication.

the problem it solves#

F-strings are wonderful and they are the most common source of injection bugs in modern Python, because they make the wrong thing effortless:

python
# this is a SQL injection and it looks completely normal
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# so is this, for HTML
return f"<div>{user_bio}</div>"

The f-string evaluates and concatenates before the function ever sees it. By the time execute gets the string, the distinction between the query template and the user data is gone permanently. There is no way for a library to help you.

what t-strings do#

A t-string does not produce a string. It produces a Template containing the static text segments and the interpolations separately:

python
from string.templatelib import Template

name = "Robert'); DROP TABLE students;--"
t = t"SELECT * FROM users WHERE name = '{name}'"

type(t)          # <class 'string.templatelib.Template'>
t.strings        # ("SELECT * FROM users WHERE name = '", "'")
t.values         # ("Robert'); DROP TABLE students;--",)

Now a library can do the right thing. A SQL driver can turn the static parts into a parameterized query and bind the values. An HTML library can escape each interpolation according to its context — attribute versus text node versus URL — which is something no escaping function can do correctly without knowing where the value landed.

python
# a hypothetical driver that accepts templates
await conn.execute(t"SELECT * FROM users WHERE name = {name}")
# becomes: SELECT * FROM users WHERE name = $1, with name bound

The syntax is identical to an f-string. Format specs and conversions work. Nesting works. The only difference is the prefix and the type.

why this is the right design#

The alternative approaches all failed for the same reason: they required the developer to do extra work at the call site, and developers under deadline do the easy thing.

t"..." is exactly as easy as f"...". It is one character. And crucially, a function that expects a Template will reject an f-string with a type error — so a library can make the safe path the only path, and the unsafe call site becomes a build failure rather than a pentest finding.

That is the property that makes this work. Security features that depend on diligence do not scale. Security features that are enforced by the type system do.

what to watch#

The value of this feature is entirely downstream: it depends on library authors adopting it. Watch for template support in the major database drivers, the templating engines, the shell-command helpers, and the logging libraries.

If that adoption happens over the next couple of release cycles, a whole category of Python vulnerability quietly stops being writable. If it does not, this is a nice piece of syntax nobody uses.

Bet on adoption. The ergonomics are too good and the security argument is too easy to make to a security team.

Dom, March 14, 2025

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →