PEP 750 – Template Strings

by grep_iton 4/10/25, 8:24 PMwith 337 comments
by Mawron 4/10/25, 11:32 PM

It's fascinating how differently languages approach the string formatting design space.

- Java's been trying to add f/t-strings, but its designers appear to be perfectionists to a fault, unable to accept anything that doesn't solve every single problem possible to imagine: [1].

- Go developers seem to have taken no more than 5 minutes considering the problem, then thoughtlessly discarded it: [2]. A position born from pure ignorance as far as I'm concerned.

- Python, on the other hand, has consistently put forth a balanced approach of discussing each new way of formatting strings for some time, deciding on a good enough implementation and going with it.

In the end, I find it hard to disagree with Python's approach. Its devs have been able to get value from first the best variant of sprintf in .format() since 2008, f-strings since 2016, and now t-strings.

[1]: https://news.ycombinator.com/item?id=40737095

[2]: https://github.com/golang/go/issues/34174#issuecomment-14509...

by nhumrichon 4/10/25, 9:22 PM

Nick Humrich here, the author who helped rewrite PEP 501 to introduce t-strings, which was the foundation for this PEP. I am not an author on this accepted PEP, but I know this PEP and story pretty well. Let me know if you have any questions.

I am super excited this is finally accepted. I started working on PEP 501 4 years ago.

by kstrauseron 4/10/25, 8:45 PM

Most excellent! I love f-strings and replaced all the various other string interpolation instances in my code with them, but they have the significant issue that you can't defer evaluating them. For instance, you can write:

  >>> template = 'Hello, {name}'
  >>> template.format(name='Bob')
  'Hello, Bob'
Until this, there wasn't a way to use f-strings formatting without interpolating the results at that moment:

  >>> template = f'Hello, {name}'
  Traceback (most recent call last):
    File "<python-input-5>", line 1, in <module>
      template = f'Hello, {name}'
                           ^^^^
  NameError: name 'name' is not defined
It was annoying being able to use f-strings almost everywhere, but str.format in enough odd corners that you have to put up with it.

by ratorxon 4/10/25, 9:04 PM

I’m not convinced that a language level feature is worth it for this. You could achieve the same thing with a function returning an f-string no? And if you want injection safety, just use a tag type and a sanitisation function that takes a string and returns the type. Then the function returning the f-string could take the Sanitised string as an argument to prevent calling it with unsanitised input.

I guess it’s more concise, but differentiating between eager and delayed execution with a single character makes the language less readable for people who are not as familiar with Python (especially latest update syntax etc).

EDIT: to flesh out with an example:

class Sanitised(str): # init function that sanitises or just use as a tag type that has an external sanitisation function.

def sqltemplate(name: Sanitised) -> str: return f”select * from {name}”

# Usage sqltemplate(name=sanitise(“some injection”))

# Attempt to pass unsanitised sqltemplate(name=“some injection”) # type check error

by simonwon 4/10/25, 8:51 PM

I'm excited about this. I really like how JavaScript's tagged template literals https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... can help handle things like automatic HTML escaping or SQL parameterization, it looks like these will bring the same capability to Python.

by spankaleeon 4/10/25, 10:39 PM

Maintainer of lit-html here, which uses tagged template literals in JavaScript extensively.

This looks really great! It's almost exactly like JavaScript tagged template literals, just with a fixed tag function of:

    (strings, ...values) => {strings, values};
It's pretty interesting how what would be the tag function in JavaScript, and the arguments to it, are separated by the Template class. At first it seems like this will add noise since it takes more characters to write, but it can make nested templates more compact.

Take this type of nested template structure in JS:

    html`<ul>${items.map((i) => html`<li>${i}</li>`}</ul>`
With PEP 750, I suppose this would be:

    html(t"<ul>{map(lambda i: t"<li>{i}</li>", items)}</ul>")
Python's unfortunate lambda syntax aside, not needing html() around nested template could be nice (assuming an html() function would interpret plain Templates as HTML).

In JavaScript reliable syntax highlighting and type-checking are keyed off the fact that a template can only ever have a single tag, so a static analyzer can know what the nested language is. In Python you could separate the template creation from the processing possibly introduce some ambiguities, but hopefully that's rare in practice.

I'm personally would be interested to see if a special html() processing instruction could both emit server-rendered HTML and say, lit-html JavaScript templates that could be used to update the DOM client-side with new data. That could lead to some very transparent fine-grained single page updates, from what looks like traditional server-only code.

by casenmgreenon 4/21/25, 8:22 AM

I read a fair part of the doc, from the start, I wanted to see how I would use t-strings in code; in the sense of, I know how I use f-strings now, in Python code, and I wanted to understand in the same way, how I would use t-strings in code. I have not understood how I would use t-strings.

by throwawayffffason 4/10/25, 9:05 PM

So we are well on our way to turning python to PHP.

Edit: Sorry I was snarky, its late here.

I already didn't like f-strings and t-strings just add complexity to the language to fix a problem introduced by f-strings.

We really don't need more syntax for string interpolation, in my opinion string.format is the optimal. I could even live with % just because the syntax has been around for so long.

I'd rather the language team focus on more substantive stuff.

by mcdeltaton 4/11/25, 1:16 AM

Could someone explain more why this should be a language feature?

My understanding of template strings is they are like f-strings but don't do the interpolation bit. The name binding is there but the values are not formatted into the string yet. So effectively this provides a "hook" into the stringification of the interpolated values, right?

If so, this seems like a very narrow feature to bake into the language... Personally, I haven't had issues with introducing some abstraction like functions or custom types to do custom interpolation.

by callamdelaneyon 4/10/25, 11:25 PM

I like this but am still not a fan of the constant adding of things to the language. It’s starting to feel like a language designed by committee, which it basically is.

by pgjoneson 4/10/25, 9:49 PM

If you want to see a usage for this I've built, and use, [SQL-tString](https://github.com/pgjones/sql-tstring) as an SQL builder.

by sgarlandon 4/10/25, 9:16 PM

Am I missing something, or is this a fancier string.Template [0]? Don't get me wrong, it looks very useful, especially the literals.

[0]: https://docs.python.org/3/library/string.html#template-strin...

by pansa2on 4/10/25, 10:30 PM

Putting aside template strings themselves for the moment, I'm stunned by some of the code in this PEP. It's so verbose! For example, "Implementing f-strings with t-strings":

    def f(template: Template) -> str:
        parts = []
        for item in template:
            match item:
                case str() as s:
                    parts.append(s)
                case Interpolation(value, _, conversion, format_spec):
                    value = convert(value, conversion)
                    value = format(value, format_spec)
                    parts.append(value)
        return "".join(parts)
Is this what idiomatic Python has become? 11 lines to express a loop, a conditional and a couple of function calls? I use Python because I want to write executable pseudocode, not excessive superfluousness.

By contrast, here's the equivalent Ruby:

    def f(template) = template.map { |item|
            item.is_a?(Interpolation) ? item.value.convert(item.conversion).format(item.format_spec) : item
        }.join

by SuperV1234on 4/10/25, 11:05 PM

This is a great PEP. Very similar to what I wanted to achieve with my P1819 for C++, back in 2019: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p18...

by illegallyon 4/11/25, 12:14 AM

Feels unnecessary...

Can't think of a good reason now on why I would need this rather than just a simple f-string.

Any unsafe string input should normally be sanitized before being added in a template/concatenation, leaving the sanitization in the end doesn't seem like the best approach, but ok.

by chaz6on 4/11/25, 4:41 PM

I like the implementation, but it looks like nobody has pointed out that Python already has a built-in string template system [1]. Granted, it's not quite as simple to use, but I have been using it for a while.

[1] https://docs.python.org/3/library/string.html#template-strin...

edit: this was mentioned by milesrout in https://news.ycombinator.com/item?id=43649607

by actinium226on 4/10/25, 10:09 PM

So does this mean that any place where code exists that looks for `type(someinstance) == str` will break because the type will be `Template` even though `someinstance` could still be used in the following code?

by ic_fly2on 4/10/25, 10:49 PM

In the past when I needed this I just made a function that processed the f string, often enough a simple lambda function would do. This looks like additional complexity for not a lot of gain.

by DonHopkinson 4/11/25, 9:22 AM

I wonder what fun The Amazing David Beazley thinks PEP 750 t-strings are?

I recently asked him:

--

Hi David! I am a huge long time fan of SWIG and your numerous epic talks on Python.

I remember watching you give a kinda recent talk where you made the point that it’s a great idea to take advantage of the latest features in Python, instead of wasting your time trying to be backwards compatible.

I think you discussed how great f-strings were, which I was originally skeptical about, but you convinced me to change my mind.

I’ve googled around and can’t find that talk any more, so maybe I was confabulating, or it had a weird name, or maybe you’ve just given so many great talks I couldn’t find the needle in the haystack.

What made me want to re-watch and link my cow-orkers to your talk was the recent rolling out of PEP 701: Syntactic formalization of f-strings, which makes f-strings even better!

Oh by the way, do you have any SWIG SWAG? I’d totally proudly wear a SWIG t-shirt!

-Don

--

He replied:

Hi Don,

It was probably the "Fun of Reinvention".

https://www.youtube.com/watch?v=js_0wjzuMfc

If not, all other talks can be found at:

https://www.dabeaz.com/talks.html

As for swag, I got nothing. Sorry!

Cheers, Dave

--

Thank you!

This must be some corollary of rule 34:

https://www.swigwholesale.com/swig-swag

(Don’t worry, sfw!)

-Don

--

The f-strings section starts at 10:24 where he's live coding Python on a tombstone with a dead parrot. But the whole talk is well worth watching, like all his talks!

https://youtu.be/js_0wjzuMfc?t=624

by mortaron 4/10/25, 11:29 PM

> If a single interpolation is expensive to evaluate, it can be explicitly wrapped in a lambda in the template string literal

I’m having trouble understanding this - Can someone please help out with an example use case for this? It seems like before with an f string we had instant evaluation, now with a t string we control the evaluation, why would we further delay evaluation - Is it just to utilise running a function on a string first (i.e. save a foo = process(bar) line?)

by oftenwrongon 4/11/25, 2:01 AM

This appears similar to Java's String Template preview feature that was withdrawn:

https://openjdk.org/jeps/465

This is probably the best overview of why it was withdrawn:

https://mail.openjdk.org/pipermail/amber-spec-experts/2024-A...

by unsnap_bicepson 4/10/25, 8:53 PM

This is super exciting but I wish they added a function to render the template to a string rather then having everyone write their own version of basic rendering.

by whoiscrobertson 4/10/25, 11:27 PM

Why should I use this instead of Template class from string

by evikson 4/11/25, 6:19 AM

With the proliferation of string type prefixes will they add one that represents a r#""# real raw string capable of ending with a backslash?

by wodenokotoon 4/11/25, 5:24 AM

I've been toying with the idea of having arbitrary string types, like

    sql"SELECT FROM ..."
or

    re"\d\d[abc]"
that the development environment could highlight properly, that would ... I don't know. In the end t and f string don't do anything that a t() and f() function couldn't have done, except they are nice. So it would be nice to have more.

by spullaraon 4/10/25, 11:02 PM

Good addition. Java has done a few previews of this and is still trying to figure out the best way. Maybe the feedback on the Python version will help.

by fmajidon 4/10/25, 10:30 PM

These templates don’t seem to be semantically aware like Go’s html/template that takes care of mitigating XSS for you, among other things.

by behnamohon 4/10/25, 8:59 PM

Is it a replacement for Jinja2 templates? I use them a lot in LLM pipelines (e.g., to fill in the system prompt and provide more context).

by throwaway7783on 4/11/25, 1:26 AM

Making t-strings not actual strings but a different type is the genius here.

It is now be a generic expression evaluator and a template rendered!

by eston 4/11/25, 1:26 AM

> If a single interpolation is expensive to evaluate, it can be explicitly wrapped in a lambda in the template string literal

> https://peps.python.org/pep-0750/#approaches-to-lazy-evaluat...

Hmm, I have a feeling there's a pitfall.

by wruzaon 4/10/25, 10:21 PM

As a python meh-er, this is actually good design. Everyone is jumping on C that it has no strings, but then other languages throw raw strings at you with some interpolation and call it a day. Also it's 2025 and people will still comment "do we need such a bloated string mechanism" and then watch new devs produce bulks of injectionable strings.

by pphyschon 4/10/25, 8:44 PM

This could be a game-changer for applications that involve lots of HTML, SQL, or other languages that tend to be embedded within Python programs. Frankly, HTML templating is the worst part of Python webdev DX for me.

Excited to see what libraries and tooling comes out of this.

by ray_von 4/11/25, 1:35 AM

Wait, I wasn't supposed to be using f strings in SQL queries?! (don't @ me)

by meiselon 4/10/25, 9:36 PM

Aside from sanitization, this also allows replication of Ruby’s %W[…] syntax

by sakesunon 4/10/25, 11:58 PM

I always think the coming of this feature is inevitable.

by apothegmon 4/11/25, 12:28 AM

Are these less useless for translation than f-strings?

by AlienRoboton 4/10/25, 9:38 PM

Thanks but I still use "%s" % (a,) the way I learned a dozen years ago and I'll keep doing it until the day I die.

by smitty1eon 4/10/25, 11:42 PM

For a quick glance, I didn't see the corner case where the old-style `%` interpolation (may) shine over PEP750: building up a JSON document with a dictionary:

>>> hello_world = {"hello":"HELL" ,"world":"O'WORLD"}

>>> json_template='{"hello":"%(hello)s","world":"%(world)s"}'

>>> print(json_template % hello_world)

{"hello":"HELL","world":"O'WORLD"}

by epistasison 4/11/25, 12:53 AM

I'm quite disappointed with the pace of development of the Python language. Five years of support for a particular version seems far too short.

I mostly use Python in scientific contexts, and hitting end-of-life after five years means that for a lot project, code needs to transition language versions in the middle of a project. Not to mention the damage to reproducibility. Once something is marked "end of life" it means that future OS versions are going to have a really good reason to say "this code shouldn't even be able to run on our new OS."

Template strings seem OK, but I would give up all new language features in a heartbeat to get a bit of long term support.

by ydnaclementineon 4/10/25, 8:54 PM

can't wait to have my linter tell me I should be using t-strings instead of f-strings. apologies for not being patient enough to read through this to find it, but I hope they can remove f-strings:

> There should be one-- and preferably only one --obvious way to do it.

by metadaton 4/10/25, 10:35 PM

What kind of special string will be added next? We already have f-strings, .format, %s ...

by otabdeveloper4on 4/11/25, 11:29 AM

Aw sweet, a third way to do string interpolation in Python.

I'm really loving this lovecraftian space the "batteries included" and "one obvious way to do it" design philosophy brought us!

by btillyon 4/10/25, 9:19 PM

I dislike this feature.

The stated use case is to avoid injection attacks. However the primary reason why injection attacks work is that the easiest way to write the code makes it vulnerable to injection attacks. This remains true, and so injection attacks will continue to happen.

Templates offer to improve this by adding interpolations, which are able to do things like escaping. However the code for said interpolations is now located at some distance from the template. You therefore get code that locally looks good, even if it has security mistakes. Instead of one source of error - the developer interpolated - you now have three. The developer forgot to interpolate, the developer chose the wrong interpolation, or the interpolation itself got it wrong. We now have more sources of error, and more action at a distance. Which makes it harder to audit the code for sources of potential error.

This is something I've observed over my life. Developers don't notice the cognitive overhead of all of the abstractions that they have internalized. Therefore over time they add more. This results in code that works "by magic". And serious problems if the magic doesn't quite work in the way that developers are relying on.

Templates are yet another step towards "more magic". With predictable consequences down the road.

by bhargavtarparaon 4/10/25, 10:51 PM

prob dont need jinja anymore then

by kazinatoron 4/11/25, 1:29 AM

15 minute implementation in TXR Lisp.

Background: TXR already Lisp has quasi-string-literals, which are template strings that do implicit interpolation when evaluated. They do not produce an object where you can inspect the values and fixed strings and do things with these before the merge.

  1> (let ((user "Bob") (greeting "how are you?"))
       `Hello @user, @greeting`)
  "Hello Bob, how are you?"
The underlying syntax behind the `...` notation is the sys:quasi expression. We can quote the quasistring and look at the car (head symbol) and cdr (rest of the list):

  2> (car '`Hello @user, @greeting`)
  sys:quasi
  3> (cdr '`Hello @user, @greeting`)
  ("Hello " @user ", " @greeting)
So that is a bit like f-strings.

OK, now with those pieces, I just right now made a macro te that gives us a template object.

  4> (load "template")
  nil
You invoke it with one argument as (te <quasistring>)

  5> (let ((user "Bob") (greeting "how are you?"))
       (te `Hello @user, @greeting`))
  #S(template merge #<interpreted fun: lambda (#:self-0073)> strings #("Hello " ", ")
              vals #("Bob" "how are you?"))
  6> *5.vals
  #("Bob" "how are you?")
  7> *5.strings
  #("Hello " ", ")
  8> *5.(merge)
  "Hello Bob, how are you?"
  9> (set [*5.vals 0] "Alice")
  "Alice"
  10> *5.(merge)
  "Hello Alice, how are you?"
You can see the object captured the values from the lexical variables, and we can rewrite them, like changing Bob to Alice. When we call the merge method on the object, it combines the template and the values.

(We cannot alter the strings in this implementation; they are for "informational purposes only").

Here is how the macro expands:

  11> (macroexpand-1 '(te `Hello @user, @greeting`))
  (new template
    merge (lambda (#:self-0073)
            (let* ((#:vals-0074
                    #:self-0073.vals)
                   (#:var-0075
                    [#:vals-0074
                      0])
                   (#:var-0076
                    [#:vals-0074
                      1]))
              `Hello @{#:var-0075}, @{#:var-0076}`))
    strings '#("Hello " ", ")
    vals (vec user greeting))
It produces a constructor invocation (new template ...) which specifies values for the slots merge, strings and vals.

The initialization of strings is trivial: just a vector of the strings pulled from the quasistring.

The vals slot is initialized by a `(vec ...)` call whose arguments are the expressions from the quasistring. This gets evaluated in the right lexical scope where the macro is expanded. This is how we capture those values.

The most complicated part is the lambda expression that initializes merge. This takes a single argument, which is the self-object, anonymized by a gensym variable for hygiene. It binds the .vals slot of the object to another gensym lexical. Then a genyms local variable is bound for each value, referencing into consecutive elements of the value vector. E.g. #:var-0075 is bound to [#:vals-0074 0], the first value.

The body of the let is a transformed version of the original template, in which the interpolated expressions are replaced by gensyms, which reference the bindings that index into the vector.

The complete implementation in template.tl (referenced by (load "template") in command line 4) is:

  (defstruct template ()
    merge
    strings
    vals)

  (defun compile-template (quasi)
    (match (@(eq 'sys:quasi) . @args) quasi
      (let ((gensyms (build-list))
            (exprs (build-list))
            (strings (build-list))
            (xquasi (build-list '(sys:quasi)))
            (self (gensym "self-"))
            (vals (gensym "vals-")))
        (while-true-match-case (pop args)
          ((@(eq 'sys:var) @(bindable @sym))
           exprs.(add sym)
           (let ((g (gensym "var-")))
             gensyms.(add g)
             xquasi.(add g)))
          ((@(eq 'sys:expr) @expr)
           exprs.(add expr)
           (let ((g (gensym "expr-")))
             gensyms.(add g)
             xquasi.(add g)))
          (@(stringp @str)
           strings.(add str)
           xquasi.(add str))
          (@else (compile-error quasi
                                "invalid expression in template: ~s" else)))
        ^(new template
              merge (lambda (,self)
                      (let* ((,vals (qref ,self vals))
                             ,*[map (ret ^(,@1 [,vals ,@2])) gensyms.(get) 0])
                        ,xquasi.(get)))
              strings ',(vec-list strings.(get))
              vals (vec ,*exprs.(get))))))

  (defmacro te (quasi)
    (compile-template quasi))
We can see an expansion:

That Lisp Curse document, though off the mark in general, was right the observation that social problems in languages like Python are just technical problems in Lisp (and often minor ones).

In Python you have to wait for some new PEP to be approved in order to get something that is like f-strings but gives you an object which intercepts the interpolation. Several proposals are tendered and then one is picked, etc. People waste their time producing rejected proposals, and time on all the bureucracy in general.

In Lisp land, oh we have basic template strings already, let's make template objects in 15 minutes. Nobody else has to approve it or like it. It will backport into older versions of the language easily.

P.S.

I was going to have the template object carry a hash of those values that are produced by variables; while coding this, I forgot. If we know that an interpolation is @greeting, we'd like to be access something using the greeting symbol as a key.

(I don't see any of this is as useful, so I don't plan on doing anything more to it. It has no place in Lisp, because for instance, we would not take anything resembling this approach for HTML generation, or anything else.)

by pjmlpon 4/10/25, 9:22 PM

Yet another way to do strings in Python, I was more than happy with the original way with tupple parameters.