This one's hot off the press as the first beta for Python 3.14 (aka. π-thon) has hit. We're looking at a chunky release with a lot of new features. But all I can think about are these new template strings (officially t-strings
).
PEP-750 officially introduces the concept. The idea is the syntax of f-strings
but allowing customised behaviours.
Quick Recap of f-strings
F-strings were introduced in Python 3.7. The allow strings to formatted for concisely:
name = "world"
f'Hello {name}'
# instead of
'Hello {name}'.format(name=name)
There are also some lesser known but very useful features.
f-string with =
Very useful for debugging, you can render both the name of the variable and the value with an =
between.
name = "Jamie"
assert f'User {name = }' == 'User name = Jamie'
f-string with formatting spec
We can also quickly format objects with f-strings, this is especially useful for formatting datetime
s:
cost = 15
assert f'{cost:.2f}' == '15.00'
assert f'{datetime(2025, 1, 1):%Y/%m/%d}' == '2025/01/01'
Why t-strings?
For me there are two big reasons for t-strings
to exist.
f-string
syntax but with custom rendering logic.f-string
but with deferred rendering.
In the PEP there are proposals to use this for html
rendering and escaping or sql
parameter substitution.
evil = "<script>alert('evil')</script>"
template = t"<p>{evil}</p>"
assert html(template) == "<p><script>alert('evil')</script></p>"
Where the t-string returns a string.templatelib.Template
object and the html
function contains the custom logic for rendering the template. In this case we delay rendering the string and then our custom logic escapes any potentially harmful variables in the string.
How does it work?
According to the PEP, the Template
object looks like:
class Template:
... # Omitted a bunch of other fields
def __iter__(self) -> Iterator[str | Interpolation]:
...
We can iterate the template for all the parts of the template. For a template of t'Hello {name}!'
becomes:
('Hello', Interpolation('jamie', 'name', None, ''), '!')
My use cases
I can see a lot of potential use cases many of them outlined in the PEP already.
Logging - a decent use case
My favourite use case so far is to solve a bit of a nit I have. The following code looks pretty innocent:
logging.debug(f"{user} - Something happened")
f-strings
are eager, so user
is stringified immediately. This is fine for simple variables but might be expensive for large nested objects. Then consider running this code in production where we turn off debug
logging, we would still need to pay the cost of calling str(user)
even if we don't log. This is the rationale behind ruff's G004.
t-strings
by nature is 'deferred', so as long as we teach logging how to handle them they will work better than f-string
here.
The following snippet does exactly that:
Now we can directly use t-strings
for logging as below:
initialize()
name = "Jamie"
logging.info(t'{name = }')
A little ugly: shorthand kwargs
So this is where I think we can take this syntax a little too far. I had this idea a few weeks ago when PEP-736 was rejected. PEP-736 comes from the observation that there are often redundant writing when it comes to invocation by keyword arguments:
User(name=name, birthday=birthday, ...)
This is actually something our t-strings
can help with. The name of the variable and the value are captured simultaneously, so we just need to then turn the t-string
into a dictionary:
from string.templatelib import Interpolation, Template
def kw(*templates: Template) -> dict[str, object]:
return {
inter.expression: inter.value
for template in templates
for inter in template.interpolations
}
Then we can apply it to a function call:
@dataclass
class User:
name: str
birthday: date
User(**kw(t"{birthday} {name}"))
User(**kw(
t"{birthday=}",
t"{name=}",
))
Honestly, I'm a little proud of this, but equally repulsed. I think it demonstrates one of the problems with t-strings
. It's the fact that they are really not strings to begin with.
They are an intermediate representation that can turn into strings, but there's no reason it has to be.
Fringe use cases like this are usually my forte, but here it looks kind of ugly and awkward to me. This coupled with the fact that we lose all typing information when we build our 't-string` make it hard to recommend here.
Should we be using t-strings then?
I think t-strings
are amazing and generally will be beneficial, I was definitely hyped about them.
I have recently shifted my opinions on new syntax in Python. I used to hate any weird syntax, going so far as to prefer dict.update(new_dict)
over the new dict |= new_dict
. But my recent experiments with DSLs showed me that done right, there is a place for new syntax to be very beneficial.
I do also see the argument against new syntax. Before writing this post, I watched anthonywritescode's video. He actually goes very in depth on t-strings
made a good argument about the added mental overhead of this.
Therefore I can only say that we need to find our balance here. For me, I'll pursue opportunities to use t-string
to generate strings. But will tread more carefully when going off-piste to create complex objects.