ZetCode

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.

first.py
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.

parts.py
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.

interpolation.py
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.

iterate.py
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.

render.py
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.

html_escape.py
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">&lt;script&gt;alert(&quot;hacked&quot;)&lt;/script&gt;</p>
<span>Tom &amp; Jerry</span>

SQL parameters

In a similar way, a t-string can be turned into a parameterized SQL query, which prevents SQL injection.

sql.py
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.

structured_log.py
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.

nested.py
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 &amp; plums</li><li>&lt;b&gt;cherries&lt;/b&gt;</li></ul>

Creating templates in code

Templates can also be created with the Template constructor and combined with the + operator.

construct.py
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.

differences.py
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

PEP 750 – Template Strings

In this article we have worked with template strings in Python. See also Python f-string.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.