A brief intro to t-strings

Template strings, also known as t-strings, are a new feature arriving in Python 3.14 later in 2025.

They're like f-strings with superpowers.

They share the same syntax:

name = "World"
my_string = f"Hello {name}!"
my_template = t"Hello {name}!"

But they are different types:

name = "World"
type(f"Hello {name}!")
# <class 'str'>
type(t"Hello {name}!")
# <class 'string.templatelib.Template'>

F-strings are just strings. But t-strings give you a new type, Template, that ships with Python 3.14. Templates let you access the parts of your string:

name = "World"
my_template = t"Hello {name}!"
list(my_template)
# ["Hello ", Interpolation(value="World"), "!"]

That Interpolation type is also new in Python 3.14. It's a fancy way of saying “this part of your string was substituted.”

Once you — the intrepid Python programmer — can get at the parts of your strings, and can know which are static and which are substituted, you can do all sorts of fun and useful things you couldn't before.

Maybe for some reason you want to render your static parts in lowercase and your substitions in uppercase?

No problem:

name = "World"
my_template = t"Hello {name}!"

def lower_upper(template: Template) -> str:
    parts: list[str] = []
    for item in template:
        if isinstance(item, str):
            parts.append(item.lower())
        else:
            parts.append(item.value.upper())
    return "".join(parts)

print(lower_upper(my_template))
# "hello WORLD!"

Okay, you probably don't want to do exactly that. But this example demonstrates that the power of t-strings comes not from the template itself, but from the code you (or someone else!) writes to process it into a string.

So why do we want t-strings?

Let's start with the useful.

A funny story: f-strings are popular. Really, really popular! They get used for all sorts of things... including things they probably shouldn't be used for.

The most common (mis)uses of f-strings lead to security vulnerabilities. We're talking Little Bobby Tables and Cross-Site Scripting (XSS). Not good at all!

Here, for your amusement, is a SQL injection vulnerability using f-strings:

from some_db_library import execute

def get_student(name: str):
    return execute(
        f"SELECT * FROM students WHERE name = '{name}'"
    )

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

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

Now, imagine using the power of t-strings instead:

from some_db_library import execute_t

def get_student(name: str):
    return execute_t(
        t"SELECT * FROM students WHERE name = '{name}'"
    )

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

That's all it takes: if your SQL library supports t-strings, it can know which parts of your string are safe and which need to be escaped. No more Bobby Tables.

Enough of the useful. Gimmie the fun!

It turns out you can do a lot of fun stuff once you have access to your string's parts.

Let's talk about HTML. Maybe you sometimes write some in Python. You can use t-strings to do this in a nice way:

from some_html_library import html

user = get_user(...)
template = t"<div data-id={user.id}>{user.name}</div>"
html(template)
# "<div data-id='123'>John</div>"

Something subtle but cool just happened. We used curly braces to include the {user.id}. But notice we didn't include quotes around the data-id attribute.

No big deal: the html() function is smart enough to see this and add the quotes for us. (And smart enough to safely escape the {user.name}, too.)

But we can do more! Wicked laughter. Much more.

What if we want to set a bunch of attributes all at once?

from some_html_library import html

user = get_user(...)
attribs = {
	"id": "user",
	"data-id": user.id,
	"alt": user.name,
}
template = t"<div {attribs}>{user.name}</div>"
html(template)
# "<div id='user' data-id='123' alt='John'>John</div>"

Wow! We just wrote a lot of HTML with very little string code.

How did that work?

The html() function can be as fancy as it wants to be. In this case, it's probably parsing HTML, looking at the type of the substitution (is it a dict? a bool?), and figuring out where in the grammar a given substition has been placed. Only then does html() know how to format the {attribs} substitution.

We can imagine all sorts of fancy HTML substitutions. Maybe we want to write "component"-like code:

from dataclasses import dataclass
from datetime import datetime
from string.templatelib import Template
from some_html_library import html

@dataclass
class Post:
	title: str
	created: datetime
	body: str

def PostHeader(title: str, created: datetime) -> Template:
	return t"""
<header>
	<h1>{title}</h1>
	<p>Posted on {created.strftime("%Y-%m-%d")}</p>
</header>
	"""

def PostBody(body: str) -> Template:
	return t"""<div class="post-body">{body}</div>"""

def render_post(post: Post) -> Template:
	return t"""
<article>
	<{PostHeader} title={post.title} created={post.created} />
	<{PostBody} body={post.body} />
</article>
	"""

post = get_post(...)
template = render_post(post)
html(template)
# "<article>
#     <header>
#         <h1>My Post</h1>
#         <p>Posted on 2023-10-01</p>
#     </header>
#     <div class="post-body">Hello, world!</div>
# </article>"

Here, the html() function looks at the substitutions, decides they are functions that can be called, and does some fancy stuff to assemble the final HTML output.

We can probably imagine all sorts of other magic, and not just for HTML!

But we'll stop here for now. There's a lot more to learn about t-strings, so check it out.