Skip to main content
Guides

Python t-strings (PEP 750): A Tested, Practical Guide

By Mithun··8 min read
Quick Answer

A t-string (PEP 750, Python 3.14) looks like an f-string but uses a t prefix and returns a `Template` object instead of a finished string. Iterating it gives you the literal text pieces and the interpolated values separately, so your code can escape, parameterise, or transform each value *before* it's combined. That's the whole point: an f-string builds the string immediately (which is how SQL-injection and XSS bugs happen), while a t-string hands you the parts so you can build it safely. Needs Python 3.14+ — it's a SyntaxError on 3.13 and earlier. Run one in our online Python compiler.

Python 3.14 added a new string literal that looks almost exactly like an f-string but behaves completely differently — the t-string (PEP 750). Swap the f prefix for a t and, instead of a finished string, you get back a `Template` object that hands you the literal text and the interpolated values *separately*.

That sounds academic until you see the payoff: because a t-string lets you inspect and transform each interpolated value *before* it's merged into the final string, it's the missing piece for safe string handling — the kind that stops SQL injection and XSS instead of inviting it. Everything below was run on Python 3.14.2; the output is real.

What a t-string is (and how it differs from an f-string)

The syntax is deliberately familiar. An f-string builds a string on the spot; a t-string of the identical shape returns a Template instead — a small object holding the pieces. The prefix is t; everything else ({expr}, !r conversions, :spec formats) is exactly what you already know from f-strings:

from string.templatelib import Template, Interpolation
name = "mithun"
role = "admin"
t = t"user {name} is {role}"
print(type(t).__name__) # Template
print([type(p).__name__ for p in t]) # ['str', 'Interpolation', 'str', 'Interpolation']
print(t.strings) # ('user ', ' is ', '')
print(t.values) # ('mithun', 'admin')
A t-string returns a Template, not a str — tested on 3.14.2

Notice what you get back: .strings is the tuple of literal text chunks *between* the holes, .values is the tuple of interpolated results, and iterating the template yields those pieces in order — a plain str for each literal, an Interpolation object for each {...}. Nothing has been combined yet. That deferral is the entire feature.

An f-string and the "same" t-string share syntax but not type: f"..." is a str the moment it's evaluated; t"..." is a Template you process later. You can't drop one in where the other is expected.

The Template and Interpolation API

Each hole becomes an Interpolation, and it carries everything the parser knew about that hole — not just the value, but the original expression text and any conversion or format spec you wrote:

qty = 5
t2 = t"total: {qty * 2!r:>6}"
interp = [p for p in t2 if isinstance(p, Interpolation)][0]
print(interp.value) # 10
print(interp.expression) # 'qty * 2'
print(interp.conversion) # 'r'
print(interp.format_spec) # '>6'
Every Interpolation exposes value, expression, conversion, format_spec

So t"total: {qty * 2!r:>6}" tells a processor: the value is 10, it came from the source expression qty * 2, apply repr (the !r), then format with >6. Your processor decides whether to honour those instructions — which is exactly what makes t-strings *programmable* where f-strings are fixed.

Rendering a t-string (PEP 750 gives you syntax, not a processor)

Here's the one gotcha everyone hits: a t-string does nothing on its own. PEP 750 defines only the literal; it deliberately ships *no* built-in renderer. To get a string back you write (or import) a template processor. This one reproduces ordinary f-string behaviour, so you can see all the moving parts in one place:

def render(template):
out = []
for part in template:
if isinstance(part, str):
out.append(part)
else: # an Interpolation
v = part.value
if part.conversion == "r":
v = repr(v)
out.append(format(v, part.format_spec))
return "".join(out)
print(render(t"user {name} is {role}")) # user mithun is admin
print(render(t"total: {qty * 2!r:>6}")) # total: 10
A minimal processor that behaves like an f-string — tested

If all you want is f-string behaviour, use an f-string — that's not why t-strings exist. Their value only shows up when the processor does something an f-string *can't*, like the safety transforms next.

The real payoff: stopping SQL injection

This is why t-strings matter for anyone shipping real software. The classic f-string mistake is building a SQL query by interpolation — which drops user input straight into the query text:

user_input = "'; DROP TABLE users; --"
# f-string: the attack is already baked into the final string
q = f"SELECT * FROM users WHERE name = '{user_input}'"
print(q)
# SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
# t-string: a processor turns each interpolation into a ? placeholder + param
def to_query(template):
sql, params = "", []
for part in template:
if isinstance(part, str):
sql += part
else:
sql += "?"
params.append(part.value)
return sql, params
sql, params = to_query(t"SELECT * FROM users WHERE name = {user_input}")
print(sql) # SELECT * FROM users WHERE name = ?
print(params) # ["'; DROP TABLE users; --"]
f-string builds the injection in; t-string parameterises it out — tested

The f-string has already concatenated the attack into the query — there's nothing left to sanitise. The t-string version never combines them: the processor walks the template, replaces every interpolation with a ? placeholder, and collects the values into a separate params list — exactly the shape cursor.execute(sql, params) expects. Same convenient syntax, but parameterised and injection-safe by construction.

t-strings don't *automatically* make anything safe — a processor still has to do the escaping or parameterising. What they give you is the one thing f-strings never could: the interpolated values *before* they're merged, so writing a safe processor is even possible.

The same trick for HTML (XSS)

The identical pattern escapes HTML. The static markup you wrote in the template is trusted; the interpolated values get html.escaped:

import html
comment = "<script>alert(1)</script>"
def safe_html(template):
out = []
for part in template:
if isinstance(part, str):
out.append(part) # trusted static markup
else:
out.append(html.escape(str(part.value))) # escape interpolated value
return "".join(out)
print(safe_html(t"<p>{comment}</p>"))
# <p>&lt;script&gt;alert(1)&lt;/script&gt;</p>
Auto-escaping interpolated values — tested

The <script> tag the user submitted comes out inert — &lt;script&gt;... — while the surrounding <p> you wrote stays real markup. That separation between *your* template and *their* data is exactly what an f-string collapses, and what a t-string keeps apart.

Which Python versions support t-strings?

t-strings are a Python 3.14 feature (shipped October 2025). They're *syntax*, so there's no __future__ import and no backport — on Python 3.13 and earlier the parser rejects the literal outright:

# Python 3.14+ -> works
t = t"hello {name}" # a Template object
# Python 3.13 and earlier -> rejected by the parser
t = t"hello {name}"
# ^^^^^^^^^^^^^^
# SyntaxError: invalid syntax
The 3.14 version wall — tested on 3.14 vs 3.11

If you need to confirm which interpreters accept a snippet, our online Python compiler defaults to Python 3.14 (so t-strings run) and lets you flip to 3.13 or 3.12 to watch the same code turn into a SyntaxError. And to see how a t-string actually compiles — it builds the Template at runtime — paste it into the bytecode disassembler.

t-string vs f-string: which should you use?

f-string (`f"..."`)t-string (`t"..."`)
ReturnsA finished strA Template object
Values combine…ImmediatelyWhen *you* process it
Access to raw partsNone.strings + .values, or iterate
Best forEveryday formattingEscaping, SQL params, safe HTML, DSLs
Available sincePython 3.6Python 3.14

Rule of thumb: reach for an f-string for everyday formatting — logs, messages, quick output. It's simpler, and t-strings aren't trying to replace it. Reach for a t-string whenever interpolated values need to be *treated* before they're combined: SQL parameters, HTML or shell escaping, structured logging, or building a small domain-specific language.

Run t-strings in your browser — free

Our online Python compiler defaults to Python 3.14, so you can paste these t-string examples and run them right now. No install, nothing uploaded.

Open the Python Compiler

Free tools mentioned here

Related guides

Frequently asked questions

What is a Python t-string?

A t-string is a new string literal in Python 3.14 (PEP 750). It looks like an f-string but uses a t prefix and returns a Template object instead of a finished str. Iterating the Template gives you the literal text pieces and the interpolated values separately, so you can escape, parameterise, or transform values before combining them.

What is the difference between t-strings and f-strings?

An f-string evaluates immediately to a str; a t-string of the same shape returns a Template object you process later. f-strings are for everyday formatting; t-strings expose the interpolated values before they're merged, which is what makes safe SQL parameterisation, HTML escaping, and custom string DSLs possible.

Do t-strings prevent SQL injection?

Not automatically — but they make prevention possible where f-strings make it impossible. A t-string hands your code each interpolated value separately, so a processor can turn them into ? placeholders with a parameter list instead of concatenating user input into the query text. An f-string has already built the final string, injection included.

How do I turn a t-string into a normal string?

PEP 750 defines only the syntax, not a renderer, so a t-string does nothing on its own. You write (or import) a template processor: iterate the Template, append the literal str parts, and format each Interpolation's .value (honouring its .conversion and .format_spec), then join the pieces.

What Python version do t-strings need?

Python 3.14 or newer. They are a syntax feature with no backport or __future__ import — on Python 3.13 and earlier a t"..." literal raises SyntaxError: invalid syntax. You can confirm this by running the same snippet on 3.14 vs 3.13 in an online Python compiler.

What is the string.templatelib module?

string.templatelib is the standard-library module that defines the Template and Interpolation types t-strings produce. A Template holds .strings (the literal chunks) and .values (the interpolated results), and iterating it yields str and Interpolation parts; each Interpolation exposes .value, .expression, .conversion and .format_spec.

Keep reading