Template strings, also known as t-strings, have been officially accepted as a feature in Python 3.14, which will ship in October 2025. 🎉

I’m excited about t-strings because they make string processing safer and more flexible. In this post, I’ll explain what t-strings are, why they were added to Python, and how you can use them.

What are t-strings?

Template strings are like f-strings with superpowers. They share the same familiar syntax:

food = "cheese"
string = f"Tasty {food}!"
template = t"Tasty {food}!"

But they result in different types:

food = "cheese"
type(f"Tasty {food}!")
# <class 'str'>
type(t"Tasty {food}!")
# <class 'string.templatelib.Template'>

F-strings are “just” strings. But t-strings give you a new type, Template, that lets you access the parts of your string:

food = "cheese"
template = t"Tasty {food}!"
list(template)
# ['Tasty ', Interpolation(value='cheese'), '!']

The Interpolation type is also new in Python 3.14. It’s a fancy way of saying “this part of your string was created by substitution.”

By giving developers access to the parts of strings, t-strings make it possible to write code that processes strings in powerful and safe ways.

Why t-strings?

Since they were introduced in Python 3.6, f-strings have become very popular. Developers use them for everything… even when they shouldn’t.

The most common (mis)uses of f-strings lead to security vulnerabilities like SQL injection and cross-site scripting (XSS).

Here’s the famous “Little Bobby Tables” vulnerability using f-strings:

from my_database_library import execute_sql

def get_student(name: str) -> dict:
    query = f"SELECT * FROM students WHERE name = '{name}'"
    return execute_sql(query)

get_student("Robert'); DROP TABLE students;--")  # ☠️ ☠️ ☠️

That execute_sql() function takes a str as input. It has no way to know whether that Robert'); nonsense was intended or not. (It wasn’t.)

Template strings solve this problem because they keep track of which parts of a string are static and which parts are dynamic. This makes it possible to write libraries that safely process user input. An updated version of the database library could look like this:

from string.templatelib import Template
from my_database_library import execute_sql_t

def get_student(name: str) -> dict:
    query = t"SELECT * FROM students WHERE name = '{name}'"
    return execute_sql_t(query)

get_student("Robert'); DROP TABLE students;--")  # 🎉 🦄 👍

No more SQL injection! The execute_sql_t() function can see that {name} is a substitution and can safely escape its value.

Importantly, Template instances are not strings. They cannot be used in places that expect a str. They cannot be concatenated with plain strings. They offer no specialized __str__() implementation; str(template) is not useful. Instead, if you want to convert a Template to a str, you must explicitly process it first.

We can imagine a library that provides an html() function that takes a Template and returns a safely escaped string:

user_supplied_input = "<script>alert('pwn')</script>"
safe = html(t"<p>{user_supplied_input}</p>")
assert safe == "<p>&lt;script&gt;alert('pwn')&lt;/script&gt;</p>"

Of course, t-strings are useful for more than just safety; they also allow for more flexible string processing. For example, that html() function could return a new type, Element. It could also accept all sorts of useful substitutions in the HTML itself:

attributes = {"src": "roquefort.jpg", "alt": "Yum"}
element = html(t"<img {attributes} />")
# You can imagine Element having a nice __str__ method:
assert str(element) == "<img src='roquefort.jpg' alt='Yum' />"

If you’ve worked with JavaScript, t-strings may feel familiar. They are the pythonic parallel to JavaScript’s tagged templates.

How do I work with t-strings?

The easiest way to get access to the parts of a Template is to iterate over it:

food = "cheese"
template = t"Tasty {food}!"
for part in template:
    if isinstance(part, str):
        print("String part:", part)
    else:
        print("Interpolation part:", part.value)
# String part: Tasty
# Interpolation part: cheese
# String part: !

You can also access the parts directly via the .strings and .values properties:

food = "cheese"
template = t"Tasty {food}!"
assert template.strings == ("Tasty ", "!")
assert template.values == (food,)

There is always one more (possibly empty) string than value. That is, t"".strings == ("",) and t"{food}".strings == ("", "").

Developers writing complex processing code can also access the gory details of each interpolation:

food = "cheese"
template = t"Tasty {food!s:>8}!"
interpolation = template.interpolations[0]
assert interpolation.value == "cheese"
assert interpolation.expression == "food"
assert interpolation.conversion == "s"
assert interpolation.format_spec == ">8"

If you’ve gone deep with f-strings in the past, you may recognize that conversion and format_spec correspond to the !s and :>8 parts of the f‑string syntax.

The expression property is a string containing the exact expression that was used inside the curly braces. This means you can use ast.parse() to analyze it further if you need to.

Most of the time, you’ll probably create Template instances using the t"..." literal syntax. But sometimes, you may want to create them programmatically:

from string.templatelib import Template, Interpolation
template = Template("Tasty ", Interpolation("cheese"), "!")

Strings and interpolations can be provided to the Template constructor in any order.

Processing t-strings

Let’s say we wanted to write code to convert all substituted words into pig latin. All it takes is a simple function:

def pig_latin(template: Template) -> str:
    """Convert a Template to pig latin."""
    result = []
    for item in template:
        if isinstance(item, str):
            result.append(item)
        else:
            word = item.value
            if word and word[0] in "aeiou":
                result.append(word + "yay")
            else:
                result.append(word[1:] + word[0] + "ay")
    return "".join(result)

food = "cheese"
template = t"Tasty {name}!"
assert pig_latin(template) == "Tasty heesecay!"

This is a goofy example; if you’d like to see some less silly examples, check out the PEP 750 examples repository.

What’s next once t-strings ship?

T-strings are a powerful new feature that will make Python string processing safer and more flexible. I hope to see them used in all sorts of libraries and frameworks, especially those that deal with user input.

In addition, I hope that the tooling ecosystem will adapt to support t-strings. For instance, I’d love to see black and ruff format t-string contents, and vscode color those contents, if they’re a common type like HTML or SQL.

It’s been fun to get to know and work with Jim, Paul, Koudai, Lysandros, and Guido on this project and to interact with many more members of the Python community online without whose input PEP 750 simply wouldn’t have come together. I can’t wait to see what developers build with t-strings once they ship!