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><script>alert('pwn')</script></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!