Python template strings
last modified October 29, 2026 Python 3.15
In this article we show how to work with template strings, also called t-strings. Template strings were added in Python 3.14 by PEP 750. All examples in this article produce the same output in Python 3.14 and 3.15.
A t-string looks like an f-string, but it uses the t prefix. An
f-string is evaluated immediately to a str. A t-string is
evaluated to a Template object, which keeps the literal text and
the interpolated values apart. Code that receives the template decides how to
combine them. This makes t-strings suitable for tasks where values must be
processed before they are inserted: escaping HTML, passing SQL parameters,
structured logging, or building shell commands.
The Template and Interpolation types and the
convert helper function live in the
string.templatelib module.
The first t-string
The first example compares an f-string with a t-string.
name = 'John'
age = 34
msg = f'{name} is {age} years old'
tpl = t'{name} is {age} years old'
print(type(msg))
print(type(tpl))
print(tpl)
The two literals differ only in their prefix.
tpl = t'{name} is {age} years old'
The t-string creates a string.templatelib.Template object. Its
representation shows the static strings and the interpolations. A template
does not render itself into text; we write the code that does it in the
following sections.
$ python first.py
<class 'str'>
<class 'string.templatelib.Template'>
Template(strings=('', ' is ', ' years old'), interpolations=(Interpolation('John', 'name', None, ''), Interpolation(34, 'age', None, '')))
Template parts
A template has three attributes that give access to its parts.
name = 'John'
age = 34
tpl = t'{name} is {age} years old'
print(tpl.strings)
print(tpl.interpolations)
print(tpl.values)
The strings attribute is a tuple of the static text pieces. It
always has one item more than the interpolations tuple; empty
strings are inserted where two interpolations are adjacent or where the
template starts or ends with an interpolation.
print(tpl.values)
The values attribute is a shortcut for the values of all
interpolations.
$ python parts.py
('', ' is ', ' years old')
(Interpolation('John', 'name', None, ''), Interpolation(34, 'age', None, ''))
('John', 34)
Interpolation objects
Each replacement field of a t-string becomes an Interpolation
object with four attributes.
price = 1234.5678
item = 'coffee'
tpl = t'{item!r:>10} costs {price:,.2f} {price=}'
for i in tpl.interpolations:
print(f'value={i.value!r} expression={i.expression!r} '
f'conversion={i.conversion!r} format_spec={i.format_spec!r}')
The value attribute holds the evaluated expression and
expression its source text. The conversion attribute
is 'r', 's', 'a', or None,
and format_spec holds the text after the colon.
tpl = t'{item!r:>10} costs {price:,.2f} {price=}'
Unlike in an f-string, the conversion and the format specification are not
applied; they are only recorded. The self-documenting {price=}
form adds the text price= to the preceding static string and sets
the conversion to 'r'.
$ python interpolation.py value='coffee' expression='item' conversion='r' format_spec='>10' value=1234.5678 expression='price' conversion=None format_spec=',.2f' value=1234.5678 expression='price' conversion='r' format_spec=''
Iterating over a template
Iterating over a template yields its non-empty static strings and its interpolations in order.
from string.templatelib import Interpolation
user = 'admin'
action = 'login'
tpl = t'User {user} performed {action}.'
for part in tpl:
match part:
case str() as text:
print(f'text: {text!r}')
case Interpolation(value, expression):
print(f'interpolation: {expression} = {value!r}')
We use pattern matching to process the two kinds of parts.
case str() as text:
print(f'text: {text!r}')
case Interpolation(value, expression):
print(f'interpolation: {expression} = {value!r}')
Static text is a plain str. The Interpolation class
supports positional patterns in the order value,
expression, conversion, and
format_spec.
$ python iterate.py text: 'User ' interpolation: user = 'admin' text: ' performed ' interpolation: action = 'login' text: '.'
Rendering like an f-string
To get the same result as an f-string, we apply the conversion and the format specification ourselves.
from string.templatelib import Interpolation, Template, convert
def render(template: Template) -> str:
parts = []
for part in template:
if isinstance(part, Interpolation):
value = convert(part.value, part.conversion)
parts.append(format(value, part.format_spec))
else:
parts.append(part)
return ''.join(parts)
item = 'coffee'
price = 3.456
print(render(t'{item!r:>10} costs {price:.2f}'))
print(f'{item!r:>10} costs {price:.2f}')
The render function walks the template and builds a string.
value = convert(part.value, part.conversion) parts.append(format(value, part.format_spec))
The convert function applies repr,
str, or ascii according to the conversion, and the
built-in format function applies the format specification. The
output matches the equivalent f-string.
$ python render.py 'coffee' costs 3.46 'coffee' costs 3.46
Escaping HTML
The main reason for t-strings is safe processing of values. The next example escapes interpolated values for HTML.
from html import escape
from string.templatelib import Interpolation, Template
def html(template: Template) -> str:
parts = []
for part in template:
if isinstance(part, Interpolation):
parts.append(escape(str(part.value)))
else:
parts.append(part)
return ''.join(parts)
comment = '<script>alert("hacked")</script>'
author = 'Tom & Jerry'
print(f'<p class="comment">{comment}</p>')
print(html(t'<p class="comment">{comment}</p>'))
print(html(t'<span>{author}</span>'))
The html function passes the static text through unchanged and
escapes only the interpolated values with html.escape.
print(f'<p class="comment">{comment}</p>')
print(html(t'<p class="comment">{comment}</p>'))
With an f-string, the malicious comment is inserted into the page as a working script tag. With a t-string, the function knows which parts came from the program and which from the outside, so it escapes the comment. The tags written by the programmer stay intact.
$ python html_escape.py
<p class="comment"><script>alert("hacked")</script></p>
<p class="comment"><script>alert("hacked")</script></p>
<span>Tom & Jerry</span>
SQL parameters
In a similar way, a t-string can be turned into a parameterized SQL query, which prevents SQL injection.
import sqlite3
from string.templatelib import Interpolation, Template
def sql(template: Template) -> tuple[str, list]:
query = []
params = []
for part in template:
if isinstance(part, Interpolation):
query.append('?')
params.append(part.value)
else:
query.append(part)
return ''.join(query), params
con = sqlite3.connect(':memory:')
con.execute('CREATE TABLE users(name TEXT, role TEXT)')
con.executemany('INSERT INTO users VALUES(?, ?)',
[('john', 'admin'), ('paul', 'user')])
name = "nobody' OR '1'='1"
query, params = sql(t'SELECT name, role FROM users WHERE name = {name}')
print(query)
print(params)
print(con.execute(query, params).fetchall())
unsafe = f"SELECT name, role FROM users WHERE name = '{name}'"
print(con.execute(unsafe).fetchall())
The sql function replaces every interpolation with a
? placeholder and collects the values in a list of parameters.
name = "nobody' OR '1'='1"
The name is a typical injection attempt. Passed as a parameter, it is treated as data, and the query finds no user of that name. The f-string version pastes the text into the SQL code, the condition becomes always true, and the query returns all users.
$ python sql.py
SELECT name, role FROM users WHERE name = ?
["nobody' OR '1'='1"]
[]
[('john', 'admin'), ('paul', 'user')]
Structured logging
Because an interpolation knows its expression, a template can produce both a readable message and machine-readable fields.
import json
from string.templatelib import Interpolation, Template
def log(template: Template) -> None:
message = ''.join(
f'{part.value}' if isinstance(part, Interpolation) else part
for part in template
)
fields = {i.expression: i.value for i in template.interpolations}
print(json.dumps({'message': message, **fields}))
user = 'john'
amount = 120
currency = 'EUR'
log(t'User {user} paid {amount} {currency}')
The log function builds the message text and a dictionary that
maps each expression to its value.
fields = {i.expression: i.value for i in template.interpolations}
The fields keep their original types; amount stays an integer in
the JSON output.
$ python structured_log.py
{"message": "User john paid 120 EUR", "user": "john", "amount": 120, "currency": "EUR"}
Nested templates
An interpolated value can be another template. A processing function can handle such values recursively, which is a simple way to build HTML from components.
from html import escape
from string.templatelib import Interpolation, Template
def html(template: Template) -> str:
parts = []
for part in template:
if not isinstance(part, Interpolation):
parts.append(part)
elif isinstance(part.value, Template):
parts.append(html(part.value))
elif isinstance(part.value, list):
parts.append(''.join(html(item) for item in part.value))
else:
parts.append(escape(str(part.value)))
return ''.join(parts)
def item(text):
return t'<li>{text}</li>'
fruits = ['apples', 'pears & plums', '<b>cherries</b>']
title = 'Fruit list'
page = t'<h2>{title}</h2><ul>{[item(f) for f in fruits]}</ul>'
print(html(page))
The item function returns a template for one list item, and the
page template contains a list of these templates.
elif isinstance(part.value, Template):
parts.append(html(part.value))
elif isinstance(part.value, list):
parts.append(''.join(html(item) for item in part.value))
Templates are rendered recursively, so their markup is kept. Only the plain values at the bottom of the structure are escaped.
$ python nested.py <h2>Fruit list</h2><ul><li>apples</li><li>pears & plums</li><li><b>cherries</b></li></ul>
Creating templates in code
Templates can also be created with the Template constructor and
combined with the + operator.
from string.templatelib import Interpolation, Template
tpl = Template('Hello ', Interpolation('John', 'name'), '!')
print(tpl)
print(tpl.strings, tpl.values)
print(Template('a', 'b'))
print(Template(Interpolation(1, 'x'), Interpolation(2, 'y')).strings)
name = 'Paul'
greeting = t'Hello {name}' + t', welcome!'
print(greeting.strings)
try:
t'Hello {name}' + ', welcome!'
except TypeError as e:
print('TypeError:', e)
The constructor accepts strings and Interpolation objects in any
order.
print(Template('a', 'b'))
print(Template(Interpolation(1, 'x'), Interpolation(2, 'y')).strings)
Consecutive strings are merged, and empty strings are inserted between
adjacent interpolations, so the strings tuple keeps its usual
shape.
greeting = t'Hello {name}' + t', welcome!'
Two templates can be concatenated. Adding a template and a str is
an error, because it would be unclear whether the string is static text or a
value.
$ python construct.py
Template(strings=('Hello ', '!'), interpolations=(Interpolation('John', 'name', None, ''),))
('Hello ', '!') ('John',)
Template(strings=('ab',), interpolations=())
('', '', '')
('Hello ', ', welcome!')
TypeError: can only concatenate string.templatelib.Template (not "str") to string.templatelib.Template
Templates are not strings
The last example shows some differences between t-strings and other string literals.
count = 1
tpl = t'count is {count}'
count = 2
print(tpl.values)
print(str(tpl) == repr(tpl))
print(t'{count}' == t'{count}')
snippets = ['ft"x"', 'tb"x"', 't"a" "b"', 'rt"C:\\temp\\{count}"']
for src in snippets:
try:
print(f'{src:22} {eval(src).strings}')
except SyntaxError as e:
print(f'{src:22} SyntaxError: {e.msg}')
The expressions of a t-string are evaluated when the template is created.
Changing count later does not change the stored value.
print(str(tpl) == repr(tpl))
print(t'{count}' == t'{count}')
A template has no special string form; str returns its
representation. Templates do not define equality either, so two templates with
the same content compare as different objects.
snippets = ['ft"x"', 'tb"x"', 't"a" "b"', 'rt"C:\\temp\\{count}"']
The t prefix cannot be combined with f,
b, or u, and a t-string cannot be implicitly
concatenated with a normal string literal. The raw prefix is allowed as
rt or tr.
$ python differences.py
(1,)
True
False
ft"x" SyntaxError: 'f' and 't' prefixes are incompatible
tb"x" SyntaxError: 'b' and 't' prefixes are incompatible
t"a" "b" SyntaxError: cannot mix t-string literals with string or bytes literals
rt"C:\temp\{count}" ('C:\\temp\\', '')
Source
string.templatelib – Support for template string literals
In this article we have worked with template strings in Python. See also Python f-string.
Author
List all Python tutorials.