by grep_it on 4/10/25, 8:24 PM with 337 comments
by Mawr on 4/10/25, 11:32 PM
- 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 nhumrich on 4/10/25, 9:22 PM
I am super excited this is finally accepted. I started working on PEP 501 4 years ago.
by kstrauser on 4/10/25, 8:45 PM
>>> 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 ratorx on 4/10/25, 9:04 PM
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 simonw on 4/10/25, 8:51 PM
by spankalee on 4/10/25, 10:39 PM
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 casenmgreen on 4/21/25, 8:22 AM
by throwawayffffas on 4/10/25, 9:05 PM
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 mcdeltat on 4/11/25, 1:16 AM
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 callamdelaney on 4/10/25, 11:25 PM
by pgjones on 4/10/25, 9:49 PM
by sgarland on 4/10/25, 9:16 PM
[0]: https://docs.python.org/3/library/string.html#template-strin...
by pansa2 on 4/10/25, 10:30 PM
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 SuperV1234 on 4/10/25, 11:05 PM
by illegally on 4/11/25, 12:14 AM
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 chaz6 on 4/11/25, 4:41 PM
[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 actinium226 on 4/10/25, 10:09 PM
by ic_fly2 on 4/10/25, 10:49 PM
by DonHopkins on 4/11/25, 9:22 AM
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!
by mortar on 4/10/25, 11:29 PM
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 oftenwrong on 4/11/25, 2:01 AM
This is probably the best overview of why it was withdrawn:
https://mail.openjdk.org/pipermail/amber-spec-experts/2024-A...
by unsnap_biceps on 4/10/25, 8:53 PM
by whoiscroberts on 4/10/25, 11:27 PM
by eviks on 4/11/25, 6:19 AM
by wodenokoto on 4/11/25, 5:24 AM
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 spullara on 4/10/25, 11:02 PM
by fmajid on 4/10/25, 10:30 PM
by behnamoh on 4/10/25, 8:59 PM
by throwaway7783 on 4/11/25, 1:26 AM
It is now be a generic expression evaluator and a template rendered!
by est on 4/11/25, 1:26 AM
> https://peps.python.org/pep-0750/#approaches-to-lazy-evaluat...
Hmm, I have a feeling there's a pitfall.
by wruza on 4/10/25, 10:21 PM
by pphysch on 4/10/25, 8:44 PM
Excited to see what libraries and tooling comes out of this.
by ray_v on 4/11/25, 1:35 AM
by meisel on 4/10/25, 9:36 PM
by sakesun on 4/10/25, 11:58 PM
by apothegm on 4/11/25, 12:28 AM
by AlienRobot on 4/10/25, 9:38 PM
by smitty1e on 4/10/25, 11:42 PM
>>> 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 epistasis on 4/11/25, 12:53 AM
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 ydnaclementine on 4/10/25, 8:54 PM
> There should be one-- and preferably only one --obvious way to do it.
by metadat on 4/10/25, 10:35 PM
by otabdeveloper4 on 4/11/25, 11:29 AM
I'm really loving this lovecraftian space the "batteries included" and "one obvious way to do it" design philosophy brought us!
by btilly on 4/10/25, 9:19 PM
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 bhargavtarpara on 4/10/25, 10:51 PM
by kazinator on 4/11/25, 1:29 AM
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 pjmlp on 4/10/25, 9:22 PM