PEP 750 – Template Strings
324 comments
·April 10, 2025Mawr
umanwizard
> 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
There are a million things in go that could be described this way.
unscaled
Looking at the various conversations involving string interpolation, this characterization is extremely unkind. They've clearly spent a lot more than 5 minutes thinking about this, including writing their own mini-proposals[1].
Are they wrong about this issue? I think they are. There is a big difference in ergonomics between String interpolation and something like fmt.Sprintf, and the performance cost of fmt.Sprintf is non-trivial as well. But I can't say they didn't put any thought into this.
As we've seen multiple times with Go generics and error handling before, their slow progress on correcting serious usability issues with the language stem from the same basic reasons we see with recent Java features: they are just being quite perfectionist about it. And unlike Java, the Go team would not even release an experimental feature unless they feel quite good about it.
mananaysiempre
> There is a big difference in ergonomics between String interpolation and something like fmt.Sprintf
On the other hand, there’s a difference in localizability as well: the latter is localizable, the former isn’t. (It also worries me that I see no substantive discussion of localization in PEP 750.)
Mawr
I just expect better from professional language designers. To me, the blindingly obvious follow up to the thought "We understand that people familiar with other languages would like to see string interpolation in Go." [1] is to research how said other languages have gone about implementing this and to present a brief summary of their findings. This is table stakes stuff.
Then there's "You can [get] a similar effect using fmt.Sprint, with custom functions for non-default formatting." [2]:
- Just the fact that "you can already do this" needs to be said should give the designers pause. Clearly you can't already do this if people are requesting a new feature. Indeed, this situation exactly mimics the story of Go's generics - after all, they do not let you do anything you couldn't do before, and yet they got added to Go. It's as if ergonomics matter, huh.
Another way to look at this: if fmt.Sprint is so good it should be used way more than fmt.Sprintf right? Should be easy to prove :)
- The argument crumbles under the load-bearing "similar effect". I already scratched the surface of why this is wrong in a sibling post: [3].
I suspect the reason for this shallow dismissal is the designers didn't go as far as to A/B test their proposal themselves, so their arguments are based on their gut feel instead of experience. That's the only way I can see someone would come up with the idea that fmt.Sprint and f-strings are similar enough. They actually are if all you do is imagine yourself writing the simplest case possible:
fmt.Sprint("This house is ", measurements(2.5), " tall")
f"This house is {measurements(2.5)} tall"
Similar enough, so long as you're willing to handwave away the need to match quotation marks and insert commas and don't spend time coding using both approaches. If you did, you'd find that writing brand new string formatting statements is much rarer than modifying existing ones. And that's where the meat of the differences is buried. Modifying f-strings is trivial, but making any changes to existing fmt.Sprint calls is painful.P.S. Proposing syntax as noisy as:
fmt.Println("This house is \(measurements(2.5)) tall")
is just another sign the designers don't get it. The entire point is to reduce the amount of typing and visual noise.[1]: https://github.com/golang/go/issues/57616#issuecomment-14509...
[2]: https://github.com/golang/go/issues/34174#issuecomment-14509...
nu11ptr
Value types anyone? I have zero doubt it is tough to add and get right, esp. to retrofit, but it has been so many years that I have learned/discarded several new languages since Java... and they STILL aren't launched yet.
mjevans
Go(lang)'s rejection makes sense.
A format function that arbitrarily executes code from within a format string sounds like a complete nightmare. Log4j as an example.
The rejection's example shows how that arbitrary code within the string could instead be fixed functions outside of a string. Safer, easier for compilers and programmers; unless an 'eval' for strings is what was desired. (Offhand I've only seen eval in /scripted/ languages; go makes binaries.)
paulddraper
No, the format function doesn't "arbitrarily execute code."
An f/t string is syntax not runtime.
Instead of
"Hello " + subject + "!"
you write f"Hello {subject}!"
That subject is simple an normal code expression, but one that occurs after the opening quote of the literal and before the ending quote of the literal.And instead of
query(["SELECT * FROM account WHERE id = ", " AND active"], [id])
you write query(t"SELECT * FROM account WHERE id = {id} AND active")
It's a way of writing string literals that if anything makes injection less likely.mjevans
Please read the context of my reply again.
The Rejected Golang proposal cited by the post I'm replying to. NOT Python's present PEP or any other string that might resolve magic variables (just not literally eval / exec functions!).
chrome111
Thanks for this example - it makes it clear it can be a mechanism for something like sqlc/typed sql (my go-to with python too, don't like orms) without a transpilation step or arguably awkward language API wrappers to the SQL. We'll need linters to prevent accidentally using `f` instead of `t` but I guess we needed that already anyways. Great to be able to see the actual cost in the DB without having to actually find the query for something like `typeddb.SelectActiveAccount(I'd)`. Good stuff.
miki123211
In many languages, f-strings (or f-string like constructs) are only supported for string literals, not user-supplied strings.
When compiling, those can be lowered to simple string concatenation, just like any for loop can be lowered to and represented as a while.
zahlman
In case there was confusion: Python's f-string functionality in particular is specific to string literals. The f prefix doesn't create a different data type; instead, the contents of the literal are parsed at compile time and the entire thing is rewritten into equivalent string concatenation code (although IIRC it uses dedicated bytecodes, in at least some versions).
The t-string proposal involves using new data types to abstract the concatenation and formatting process, but it's still a compile-time process - and the parts between the braces still involve code that executes first - and there's still no separate type for the overall t-string literal, and no way to end up eval'ing code from user-supplied data except by explicitly requesting to do so.
mjevans
My reply was to the parent post's SPECIFIC example of Golang's rejected feature request. Please go read that proposal.
It is NOT about the possibility of referencing existing / future (lazy / deferred evaluation) string literals from within the string, but about a format string that would literally evaluate arbitrary functions within a string.
NoTeslaThrow
What's the risk of user supplied strings? Surely you know their size. What else is there to worry about?
NoTeslaThrow
> A format function that arbitrarily executes code from within a format string
So, a template? I certainly ain't gonna be using go for its mustache support.
bcoates
No, it's exactly the opposite--f-strings are, roughly, eval (that is, unsanitary string concatenation that is presumptively an error in any nontrivial use) to t-strings which are just an alternative expression syntax, and do not even dereference their arguments.
rowanG077
f-strings are not eval. It's not dynamic. It's simply an expression that is ran just like every other expression.
cherry_tree
>Go developers seem to have taken no more than 5 minutes considering the problem, then thoughtlessly discarded it
The issue you linked was opened in 2019 and closed with no new comments in 2023, with active discussion through 2022.
cortesoft
Then there is Ruby, which just has beautiful string formatting without strange decorators.
bshacklett
That tracks. Ruby followed in the footsteps of Perl, which had string manipulation as a main priority for the language.
thayne
That issue has a link to another Issue with more discussion: https://github.com/golang/go/issues/57616.
But as is all too common in the go community, there seems to be a lot of confusion about what is proposed, and resistance to any change.
1980phipsi
D had a big blow up over string interpolation. Walter wanted something simple and the community wanted something more like these template ones from Python (at least from scanning the first little bit of the PEP). Walter eventually went with what the community wanted.
gthompson512
This led to the OpenD language fork (https://opendlang.org/index.html) which is led by some contributors who had other more general gripes with D. The fork is trying to merge in useful stuff from main D, while advancing the language. They have a Discord which unfortunately is the main source of info.
throwaway2037
I promise, no trolling from me in this comment. I never understood the advantage of Python f-strings over printf-style format strings. I tried to Google for pros and cons and didn't find anything very satisfying. Can someone provide a brief list of pros and cons? To be clear, I can always do what I need to do with both, but I don't know f-strings nearly as well as printf-style, because of my experience with C programming.
Mawr
Sure, here are the two Go/C-style formatting options:
fmt.Sprintf("This house is %s tall", measurements(2.5))
fmt.Sprint("This house is ", measurements(2.5), " tall")
And the Python f-string equivalent: f"This house is {measurements(2.5)} tall"
The Sprintf version sucks because for every formatting argument, like "%s", we need to stop reading the string and look for the corresponding argument to the function. Not so bad for one argument but gets linearly worse.Sprint is better in that regard, we can read from left to right without interruptions, but is a pain to write due to all the punctuation, nevermind refactor. For example, try adding a new variable between "This" and "house". With the f-string you just type {var} before "house" and you're done. With Sprint, you're now juggling quotation marks and commas. And that's just a simple addition of a new variable. Moving variables or substrings around is even worse.
Summing up, f-strings are substantially more ergonomic to use and since string formatting is so commonly done, this adds up quickly.
throwaway2037
> Not so bad for one argument but gets linearly worse.
This is a powerful "pro". Thanks.nhumrich
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.
Waterluvian
I often read concerns that complexity keeps being added to the language with yet another flavour of string or whatnot. Given that those who author and deliberate on PEPs are, kind of by definition, experts who spend a lot of time with the language, they might struggle to grok the Python experience from the perspective of a novice or beginner. How does the PEP process guard against this bias?
rtpg
There are many long-term users of Python who participate in PEP discussion who argue for beginners[0], often because they professionally are teaching Python.
There are also loads of people basically defaulting to "no" on new features, because they understand that there is a cost of supporting things. I will often disagree about the evaluation of that cost, but it's hard to say there is no cost.
Nobody wants a system that is unusable, slow, hard to implement for, or hard to understand. People sometimes just have different weights on each of these properties. And some people are in a very awkward position of overestimating costs due to overestimating implementation effort. So you end up in discussions like "this is hard to understand!" "No it isn't!"
Hard to move beyond, but the existence of these kinds of conversations serve, in a way, as proof that people aren't jumping on every new feature. Python is still a language that is conservative in what it adds.
This should actually inspire more confidence in people that features added to Python are _useful_, because there are many people who are defaulting to not adding new features. Recent additions to Python speeding up is more an indicator of the process improving and identifying the good stuff rather than a lowering of the bar.
[0]: I often think that these discussions often get fairly intense. Understandability is definitely a core Python value, but I Think sometimes discussions confuse "understandability" with "amount of things in the system". You don't have to fully understand pervasive hashing to understand Python's pervasive value equality semantics! A complex system is needed to support a simple one!
nhumrich
All discussion on PEP's happens in public forums where anyone can opine on things before they are accepted. I agree that the experts are more likely to participate in this exchange. And while this is wish-washy, I feel like the process is really intended to benefit the experts more than the novices anyways.
There have been processes put into place in recent years to try to curb the difficulty of things. One of those is that all new PEPs have to include a "how can you teach this to beginers" section, as seen here on this pep: https://peps.python.org/pep-0750/#how-to-teach-this
Waterluvian
I think "how can you teach this to beginners?" is a fantastic, low-hanging fruit option for encouraging the wizards to think about that very important type of user.
Other than a more broad "how is the language as a whole faring?" test, which might be done through surveys or other product-style research, I think this is just plainly a hard problem to approach, just by the nature that it's largely about user experience.
anon-3988
The average Python developer does not even know what a "PEP" is. Open discussion is good yes, but no one really knows what the average developer wants because they simply does not care if its Python or Java or whatever else.
"Some hammers are just shaped weird, oh well, just make do with it."
For example, some people that I interview does not "get" why you have to initialize the dict before doing dict[k] += 1. They know that they have to do some ritual of checking for k in dict and dict[k] = 0. But they don't get that += desugars into dict[k] = dict[k] + 1.
davepeck
You might find the Python discussion forums ([0] and [1]) interesting; conversation that guides the evolution of PEPs happens there.
As Nick mentioned, PEP 750 had a long and winding road to its final acceptance; as the process wore on, and the complexities of the earliest cuts of the PEPs were reconsidered, the two converged.
[0] The very first announcement: https://discuss.python.org/t/pep-750-tag-strings-for-writing...
[1] Much later in the PEP process: https://discuss.python.org/t/pep750-template-strings-new-upd...
jackpirate
Building off this question, it's not clear to me why Python should have both t-strings and f-strings. The difference between the two seems like a stumbling block to new programmers, and my "ideal python" would have only one of these mechanisms.
nhumrich
f-strings immediately become a string, and are "invisible" to the runtime from a normal string. t-strings introduce an object so that libraries can do custom logic/formatting on the template strings, such as decided _how_ to format the string.
My main motivation as an author of 501 was to ensure user input is properly escaped when inserting into sql, which you cant enforce with f-strings.
davepeck
For one thing, `f"something"` is of type `str`; `t"something"` is of type `string.templatelib.Template`. With t-strings, your code can know which parts of the string were dynamically substituted and which were not.
skeledrew
Give it a few years to when f-string usage has worn off to the point that a decision can be made to remove it without breaking a significant number of projects in the wild.
patrec
My memory is that ES6's template strings preceded f-strings. If that is correct, do you happen to know why python was saddled with f-strings, which seem like an obviously inferior design, in the first place? We are now at five largely redundant string interpolation systems (%, .format, string.Template, f-string, t-string).
nhumrich
PEP 501 when originally written (not by me) was intended to be the competing standard against f-strings, and to have been more inline with ES6's template strings. There was debate between the more simple f-string PEP (PEP 498) and PEP 501. Ultimately, it was decided to go with f-strings as a less confusing, more approachable version (and also easier to implement) and to "defer" PEP 501 to "see what happens". Since then, the python internal have also changed, allowing t-strings to be even easier to implement (See PEP 701). We have seen what happens, and now its introduced. f-strings and t-strings are not competing systems. They are different. Similar to ES6 templates and namedTaggedTemplates, they are used for different things while API feels similar intentionally. f-strings are not inferior to t-strings, they are better for most use cases of string templating where what you really want, is just a string.
patrec
Thanks!
> they are better for most use cases of string templating where what you really want, is just a string.
I think use cases where you want to unconditionally bash a string together are rare. I'd bet that in > 80% of cases the "just a string" really is just a terrible representation for what really is either some tree (html, sql, python, ...) structure or at least requires lazy processing (logging, where you only want to pay for the expensive string formatting and generation if you run at the log level or higher that the relevant logging line is meant to operate).
mardifoufs
I'm not familiar with ES6 template strings, but why are they better than f-strings? F-strings just work, and work well, in my experience so I'm wondering what I'm missing out on. Especially since the language I use the most is c++... So I guess I don't expect much out of string manipulation lol.
patrec
The problem with f-strings is that they make an extremely limited use case convenient (bashing unstructured text) and thus people people invariably use them for the less limited use case for which no such covenient mechanism exists. Constructing ASTs (including html and SQL). Or logging (where you want to avoid unconditionally computing some expensive string represenation).
I do this myself. I basically always use the subtl wrong log.warning(f"Unexpected {response=} encountered") and not the correct, and depending on the loglevel cheaper log.warning("Unexpected respone=%s encountered", repsonse). The extra visual noise is typically not worth the extra correctness and performance (I'd obviously not do this in some publically exposed service receiving untrusted inputs).
I'd argue these use cases are in fact more prevalent then the bashing unstructured text use case.
Encouraging people to write injection vulnerabilities or performance and correcness bugs isn't great language design.
WorldMaker
ES2015 template strings from the beginning supported "tagged template literals" where the tag is a function that gets the template itself and the objects passed to the "holes" in the template as separate arguments. From there that function can do things like turn the holes themselves into something like SQL Parameter syntax and wrap the things that go in those holes in properly escaped SQL Parameters.
`some template {someVar}` was f-strings and someFunction`some template {someVar}` was more like what these t-strings provide to Python. t-strings return an object (called Template) with the template and the things that go into the "holes", versus tagged templates are a function calling pattern, but t-strings are still basically the other, richer half of ES2015+ template strings.
_cs2017_
Thank you! Curious what options for deferred evalution were considered and rejected? IMHO, the main benefit of deferred evaluation isn't in the saving of a bit of code to define a deferred evaluation class, but in standardazing the API so that anyone can read the code without having to learn what it means in each project.
Also: were prompt templates for LLM prompt chaining a use case that influenced the design in any way (examples being LangChain and dozens of other libraries with similar functionlity)?
nhumrich
One solution that existed for a while was using the `!` operator for deferred. `t!'my defered {str}'`
The main reason for non having deferred evaluation was that it over-complicated the feature quite a bit and introduces a rune. Deferred evaluation also has the potential to dramatically increase complexity for beginners in the language, as it can be confusing to follow if you dont know what is going on. Which means "deferred by default" wasnt going to be accepted.
As for LLM's, it was not the main consideration, as the PEP process here started before LLM's were popular.
_cs2017_
Ah interesting, so the complexity wasn't in the API design or implementation, but only in the additional rune? Is that really such a big cost?
smnrchrds
Thank you for your work on this topic and for answering questions here. I have a question: is there a way to avoid the security issues with string formatting described here? It seems like all (most?) string formatting options suffer from the same issue.
https://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
leobuskin
As I understand, it may help a bit with logging performance, not sure, still trying to understand the template abilities.
So, right now, you have two options to log:
1. `logger.debug(f'Processing {x}')` - looks great, but evaluates anyway, even if logging level > `logging.DEBUG`;
2. `logger.debug('Processing %s', x)` - won't evaluate till necessary.
What would be the approach with t-strings in this case? Would we get any benefits?
bcoates
The expression (x) is eagerly evaluated in both cases, cuz that's how Python works. You can defer the format call but Python fundamentally doesn't have an equivalent of lazy/compile time flag argument evaluation and this doesn't change that.
For a logger t-strings are mostly just a more pleasant and less bug-prone syntax for #2
davepeck
T-strings, like f-strings, are eagerly evaluated -- so in this sense, no, there's no benefit.
trashburger
Not quite; the interpolations are not eagerly stringified which is the potentially expensive part. In this sense it's kind of a middle ground between the two approaches.
frainfreeze
Nice work on PEP 501! Probably a silly question, but how comes PEP 292 isn't mentioned anywhere in PEP 750?
davepeck
My hope is to write some new documentation as 3.14 nears release that explains the (growing) constellation of string formatting mechanisms in Python and describes when they might each be useful. They overlap to some degree, but each has a unique twist that makes them useful in different situations. PEP 292 is going nowhere and is used, for instance, in really powerful libraries like `flufl.i18n`
sevensor
Is a PEP 750 Template entirely different from a PEP 292 Template? I’m a bit confused about the relationship.
bjourne
Does Python really need yet another type of string literal? I feel like while templating is a good addition to the standard library, it's not something that needs syntactic support. t"blah blah" is just an alias for Template("blah blah", context), isn't it?
nhumrich
yes, it does actually need syntax support. In order for it to work, you need to preserve which parts of the string are static (hard coded) and which parts are dynamic (likely user input). Which you can only do at a syntax level. You could potentially do it by hand, using placeholders, like with `%`, but as we now live in f-string world, we have something better. The syntax highlighting and ergonomics of f-strings are so good, devs prefer it in most cases. The idea is to make the most ergonomic thing, also the safest thing. By decreasing ergonomics, you reduce the adoption of safer symantics.
bjourne
That's why I specified the context argument. Something like Template("{name} {address}", dict(name = ..., address = ...)) would be exactly equivalent to t"{name} {address}" assuming those variables are fetched from the local scope.
thayne
A library can't capture interpolated variables
EndsOfnversion
[flagged]
kstrauser
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.ratorx
Delayed execution is basically equivalent to a function call, which is already a thing. It also has basically the same API as point of use and requires maybe 1 extra line.
null
paulddraper
You gravely misunderstand.
The point of evaluation of the expressions is the same.
>>> template = t'Hello, {name}'
is still an error if you haven't defined name.BUT the result of a t-string is not a string; it is a Template which has two attributes:
strings: ["Hello, ", ""]
interpolations: [name]
So you can then operate on the parts separately (HTML escape, pass to SQL driver, etc.).ossopite
I'm not sure if t-strings help here? unless I misread the PEP, it seems like they still eagerly evaluate the interpolations.
There is an observation that you can use `lambda` inside to delay evaluation of an interpolation, but I think this lambda captures any variables it uses from the context.
actinium226
> There is an observation that you can use `lambda` inside to delay evaluation of an interpolation, but I think this lambda captures any variables it uses from the context.
Actually lambda works fine here
>>> name = 'Sue'
>>> template = lambda name: f'Hello {name}'
>>> template('Bob')
'Hello Bob'
davepeck
> I'm not sure if t-strings help here?
That's correct, they don't. Evaluation of t-string expressions is immediate, just like with f-strings.
Since we have the full generality of Python at our disposal, a typical solution is to simply wrap your t-string in a function or a lambda.
(An early version of the PEP had tools for deferred evaluation but these were dropped for being too complex, particularly for a first cut.)
krick
And that actually makes "Template Strings" a misnomer in my mind. I mean, deferred (and repeated) evaluation of a template is the thing that makes template a template.
Kinda messy PEP, IMO, I'm less excited by it than I'd like to be. The goal is clear, but the whole design feels backwards.
notpushkin
> An early version of this PEP proposed that interpolations should be lazily evaluated. [...] This was rejected for several reasons [...]
Bummer. This could have been so useful:
statement_endpoint: Final = "/api/v2/accounts/{iban}/statement"
(Though str.format isn’t really that bad here either.)nhumrich
There was a very very long discussion on this point alone, and there are a lot of weird edge cases, and led to weird syntax things. The high level idea was to defer lazy eval to a later PEP if its still needed enough.
There are a lot of existing workarounds in the discussions if you are interested enough in using it, such as using lambdas and t-strings together.
LordShredda
Would be useful in that exact case, but would be an absolute nightmare to debug, on par with using global variables as function inputs
o11c
I do think that people are far too hesitant to bind member functions sometimes:
statement_endpoint: Final = "/api/v2/accounts/{iban}/statement".format
(it's likely that typecheckers suck at this like they suck at everything else though)tylerhou
You could do something like t”Hello, {“name”}” (or wrap “name” in a class to make it slightly less hacky).
skeledrew
Lambda only captures variables which haven't been passed in as argument.
davepeck
PEP 750 doesn't directly address this because it's straightforward to simply wrap template creation in a function (or a lambda, if that's your style):
def my_template(name: str) -> Template:
return t"Hello, {name}"
foobahify
The issue was solved by having a rich and Turing complete language. I am not huge on adding language features. This seems like userland stuff.
actinium226
I tend to agree. I think it's easy enough to use a lambda in this case
>>> template = lambda name: f'Hello {name}'
>>> template('Bob')
catlover76
[dead]
ratorx
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
nhumrich
> You could achieve the same thing with a function returning an f-string no no.
> just use a tag type and a sanitisation function that takes a string and returns the type
Okay, so you have a `sqlstring(somestring)` function, and the dev has to call it. But... what if they pass in an f-string?
`sqlstring(f'select from mytable where col = {value}')`
You havent actually prevented/enforced anything. With template strings, its turtles all the way down. You can enforce they pass in a template and you can safely escape anything that is a variable because its impossible to have a variable type (possible injection) in the template literal.
ratorx
Added example to parent comment.
This example still works, the entire f-string is sanitised (including whatever the value of name was). Assuming sqlstring is the sanitisation function.
The “template” would be a separate function that returns an f-string bound from function arguments.
nhumrich
Yes. Only if your dev remembers to use sanatized all the time. This is how most SQL works today. You could also forget and accidentally write a f-string, or because you dont know. But with t-strings you can actually prevent unsanatized inputs. With your example, you need to intentionally sanitize still.
You cant throw an error on unsanitized because the language has no way to know if its sanitized or not. Either way, its just a string. "returning an f-string" is equivalent to returning a normal string at runtime.
shikon7
If its only use is to make injecton safety a bit easier to achieve, it's worth it to me.
ratorx
Does it make it easier? The “escape” for both is to just use unsafe version of the Template -> string function or explicitly mark an unsafe string as sanitised. Both seem similar in (un)safety
davepeck
> the Template -> string function
There is no such function; Template.__str__() returns Template.__repr__() which is very unlikely to be useful. You pretty much have to process your Template instance in some way before converting to a string.
vjerancrnjak
It's worse than function returning an f-string. Template type is very flat, you won't know which arguments are left unbound.
modules, classes, protocols, functions returning functions, all options in Python, each work well for reuse, no need to use more than 2 at once, yet the world swims upstream.
itishappy
I don't see how this prevents calling your returned f-string with unsensitized inputs.
evil = "<script>alert('evil')</script>"
sanitized = Sanitized(evil)
whoops = f"<p>{evil}</p>"
ratorx
I’m not sure you understood my example. The f-string is within a function. The function argument only accepts sanitised input type.
If you create a subclass of str which has an init function that sanitises, then you can’t create a Sanitised type by casting right?
And even if you could, there is also nothing stopping you from using a different function to “html” that just returns the string without sanitising. They are on the same relative level of safety.
itishappy
Oh, I'm pretty sure I didn't understand your example and am probably missing something obvious. That's why I'm here asking dumb questions!
I think I'm following more, and I see how you can accomplish this by encapsulating the rendering, but I'm still not seeing how this is possible with user facing f-strings. Think you can write up a quick example?
simonw
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.
davepeck
Yes! PEP 750 landed exactly there: as a pythonic parallel to JavaScript's tagged template strings. I'm hopeful that the tooling ecosystem will catch up soon so we see syntax coloring, formatting of specific t-string content types, etc. in the future.
its-summertime
I just wish it didn't get Pythonified in the process, e.g. needing to be a function call because backtick is hard to type on some keyboards, nearly having a completely separate concept of evaluating the arguments, etc. x`` vs x(t'') is a 2x blowup in terms of line-noise at worst.
itishappy
I don't think that's the reason.
https://peps.python.org/pep-0750/#arbitrary-string-literal-p...
its-summertime
that gets solved by using a different quoting character for template literals.
https://discuss.python.org/t/pep-750-tag-strings-for-writing...
> Backticks are traditionally banned from use in future language features, due to the small symbol. No reader should need to distinguish ` from ' at a glance. It’s entirely possible that the prevailing opinion on this has changed, but it’s certainly going to be easier to stick to the letter prefixes and regular quotes.
and earlier in the thread the difficulty in typing it for some is cited as another reason
spankalee
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.
davepeck
> assuming an html() function would interpret plain Templates as HTML
Agreed; it feels natural to accept plain templates (and simple sequences of plain templates) as HTML; this is hinted at in the PEP.
> html(t"<ul>{map(lambda i: t"<li>{i}</li>", items)}</ul>")
Perhaps more idiomatically: html(t"<ul>{(t"<li>{i}</li>" for i in items)}</ul>")
> syntax highlighting and type-checking are keyed off the fact that a template can only ever have a single tag
Yes, this is a key difference and something we agonized a bit over as the PEP came together. In the (very) long term, I'm hopeful that we see type annotations used to indicate the expected string content type. In the nearer term, I think a certain amount of "clever kludginess" will be necessary in tools like (say) black if they wish to provide specialized formatting for common types.
> 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
I'd love to see this and it's exactly the sort of thing I'm hoping emerges from PEP 750 over time. Please do reach out if you'd like to talk it over!
MathMonkeyMan
Python already has built-in data structure literals that allow you to express lispy DSLs:
html(['ul', {'class': 'foo'}, *(['li', item] for item in items)])
I guess template strings do make it more concise. Kind of like Racket's "#lang at-exp racket".The benefit of lisp-like representation is you have the entire structure of the data, not just a sequence of already-serialized and not-yet-serialized pieces.
Latty
Generally a generator expression would be more idiomatic in Python than map/lambda.
html(t"<ul>{(t"<li>{i}</li>" for i in items)}</ul>")
pauleveritt
I'd like to add, after the first publication for discussion, we got some wonderful involvement from Andrea Giammarchi who brought his deep JS libraries and tools experience into the PEP. In fact, he's deeply involved in the next steps, with some forthcoming demos and libraries that will make a real difference. Exciting times.
breuleux
> not needing html() around nested template could be nice
One possibility would be to define __and__ on html so that you can write e.g. html&t"<b>{x}</b>" (or whichever operator looks the best).
throwawayffffas
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.
turtledragonfly
> turning python to PHP.
Why stop there? Go full Perl (:
I think Python needs more quoting operators, too. Maybe qq{} qq() q// ...
[I say this as someone who actually likes Perl and chuckles from afar at such Python developments. May you get there one day!]
tdeck
Quoting operators are something I actually miss in Python whereas t-strings are something I have never wanted in 17 years of writing Python.
mardifoufs
What's the issue with f-strings? I'm wondering because I thought they basically had no downside versus using the older alternatives. I use them so often that they are very substantive to me. If anything, this is exactly what python should be focusing on, there really isn't a lot more that they can do considering the design, expectations, and usage of python.
throwawayffffas
In the motivation for the t-string types, their gripe is that f-strings are not templates.
My issue with them is that you have to write your syntax in the string complex expressions dictionary access and such become awkward.
But, this whole thing is bike-shedding in my opinion, and I don't really care about the color of the bike shed.
nhumrich
Pretty sure PHP does not have this feature. Can you give me an example?
fshr
I believe that jab was that PHP has a bunch of ways to do similar things and Python, in their view, is turning out that way, too.
throwawayffffas
On a more philosophical level php is this feature. At least as it was used originally and how it's mostly used today. PHP was and is embedded in html code. If you have a look at a wordpress file you are going to see something like this:
<?php ... ?><some_markup>...<? php ... ?><some_more_markup here>...
slightwinder
string.format and string substitution are bloat and annoying to use, while f-strings makes it very easy to improve readability. So in the end, they remove big complexity in usage, by adding very little and straightforward complexity in syntax.
pgjones
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.
sgarland
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...
zahlman
string.Template does fundamentally the same sort of thing as str.format, or str.__mod__ (i.e. the % operator on strings). It was in a sense a prototype for str.format, introduced way back in Python 2.4 (https://peps.python.org/pep-0292/). It does create an instance of a separate type, but it neither pre-parses the string (the substitution is implemented using regexes that reprocess the original string each time) nor eagerly evaluates the values to substitute. Further, in order to change how the formatting is done, you'd have to subclass (with the new system, you just write a function that accepts the Template as a parameter).
sgarland
Thank you for the historical context and details!
mcdeltat
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.
yoru-sulfur
It reminds me of javascripts template literals (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...) and .nets FormattableString (https://learn.microsoft.com/en-us/dotnet/api/system.formatta...)
The best use case I know of for these kinds of things is as a way to prevent sql injection. SQL injection is a really annoying attack because the "obvious" way to insert dynamic data into your queries is exactly the wrong way. With a template string you can present a nice API for your sql library where you just pass it "a string" but it can decompose that string into query and arguments for proper parameterization itself without the caller having to think about it.
rcxdude
You can see some motivations for it further down the document. Basically it allows libraries to access the ease of use of f-strings for more than just formatting a string according to what the language allows. Structured logging is one area where I would like to use this.
duped
It's useful for doing things like writing parameterized SQL queries that avoid injection attacks. Or anything that quacks like that proverbial duck, where you are going to emit some plaintext and need to validate it as it is filled.
anonylizard
Because python dominates AI, and python is dominating because of AI. And prompting really, really benefits from f-strings and templated strings. LLMs as a whole means the rise of unstructured data, and flexible string manipulation is really important for handling that.
callamdelaney
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.
Daishiman
It's a feature that removes the need for hundreds of specialized template classes in various frameworks and is a generalization of an existing feature in a way that doesn't add much in the way of grammar. The net complexity reduction is a win here.
pansa2
> It’s starting to feel like a language designed by committee, which it basically is.
That's exactly what it is. It's just that they use the word "council" instead of "committee".
smitty1e
Python is released annually and having some new juice helps advertise the language.
Whether or not this is technically a swift call is in the eye of the beholder.
cbmask
[dead]
pansa2
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
davepeck
We wanted at least a couple examples that showed use of Python's newer pattern matching capabilities with the new Interpolation type. From this outsider's perspective, I'd say that developer instincts and aesthetic preferences are decidedly mixed here -- even amongst the core team! You can certainly write this as:
def f(template: Template) -> str:
return "".join(
item if isinstance(item, str) else
format(convert(item.value, item.conversion), item.format_spec)
for item in template
)
Or, y'know, several other ways that might feel more idiomatic depending on where you're coming from.pansa2
Your example is interesting - `join`, a generator expression and a ternary, and Python requires us to write all of them inside-out. It's a shame we can't write that in a more natural order, something like:
def f(template):
return (for item in template:
isinstance(item, str) then item else
format(convert(item.value, item.conversion), item.format_spec)
).join('')
davepeck
Yeah. `join` has forever been backwards to me in Python and I still sometimes get it wrong the first time out.
Comprehensions, though -- they are perfection. :-)
the-grump
This is how Python has always been. It's more verbose and IMO easier to grok, but it still lets you create expressive DSLs like Ruby does.
Python has always been my preference, and a couple of my coworkers have always preferred Ruby. Different strokes for different folks.
pansa2
> This is how Python has always been.
Nah, idiomatic Python always used to prefer comprehensions over explicit loops. This is just the `match` statement making code 3x longer than it needs to be.
the-grump
You can express the loop as a list comprehension, and I would too.
As for the logic, I would still use pattern matching for branching and destructuring, but I’d put it in a helper. More lines is not a negative in my book, though I admit the thing with convert and format is weird.
zahlman
A compromise version:
def _f_part(item) -> str:
match item:
case str() as s:
return s
case Interpolation(value, _, conversion, format_spec):
return format(convert(value, conversion), format_spec)
def f(template: Template) -> str:
return ''.join(map(_f_part, template))
The `match` part could still be written using Python's if-expression syntax, too. But this way avoids having very long lines like in the Ruby example, and also destructures `item` to avoid repeatedly writing `item.`.I very frequently use this helper-function (or sometimes a generator) idiom in order to avoid building a temporary list to `.join` (or subject to other processing). It separates per-item processing from the overall algorithm, which suits my interpretation of the "functions should do one thing" maxim.
Mawr
The Python version is straightforward to read and understand to a programmer of any language. The Ruby version is an idiosyncratic symbol soup.
If I were tasked to modify the Python version to say, handle the case where `item` is an int, it would be immediately obvious to me that all I need to do is modify the `match` statement with `case int() as i:`, I don't even need to know Python to figure that out. On the other hand, modifying the Ruby version seems to require intimate knowledge of its syntax.
pansa2
I think for someone with a basic knowledge of both languages, the Ruby version is more understandable than the Python. It's a combination of basic Ruby features, whereas Python's `match` statement is much more obscure - it isn't really Python at all, it's "a DSL contrived to look like Python [...] but with very different semantics" [0].
I don't particularly love the Ruby code either, though - I think the ideal implementation would be something like:
fn stringify(item) =>
item.is_a(Interpolation) then
item.value.convert(item.conversion).format(item.format_spec)
else item.to_string()
fn f(template) => template.map(stringify).join()
[0] https://discuss.python.org/t/gauging-sentiment-on-pattern-ma...slightwinder
> Is this what idiomatic Python has become?
What do you mean? Python has always been that way. "Explicit is better than implicit. [..] Readability counts." from the Zen of python.
> By contrast, here's the equivalent Ruby:
Which is awful to read. And of course you could write it similar short in python. But it is not the purpose of a documentation to write short, cryptic code.
pansa2
> Readability counts
Almost all Python programmers should be familiar with list comprehensions - this should be easy to understand:
parts = [... if isinstance(item, Interpolation) else ... for item in template]
Instead the example uses an explicit loop, coupled with the quirks of the `match` statement. This is much less readable IMO: parts = []
for item in template:
match item:
case str() as s:
parts.append(...)
case Interpolation(value, _, conversion, format_spec):
parts.append(...)
> [Ruby] is awful to readI think for someone with a basic knowledge of Ruby, it's more understandable than the Python. It's a combination of basic Ruby features, nothing advanced.
I don't particularly love Ruby's syntax either, though - I think the ideal implementation would be something like:
fn stringify(item) =>
item.is_a(Interpolation) then
item.value.convert(item.conversion).format(item.format_spec)
else item.to_string()
fn f(template) => template.map(stringify).join()
slightwinder
> Almost all Python programmers should be familiar with list comprehensions
Being familiar doesn't mean it's readable. They can be useful, but readability is usually not on that list.
> I think for someone with a basic knowledge of Ruby, it's more understandable than the Python.
I know both, and still consider it awful. Readability is not about making it short or being able to decipher it.
pphysch
That Ruby code is clever and concise, and terrible to read and extend
pansa2
IMO it's much closer to the ideal way to write the function, which would be something like:
fn stringify(item) =>
item.is_a(String) then item else
item.value.convert(item.conversion).format(item.format_spec)
fn f(template) => template.map(stringify).join()
pphysch
By what definition of "ideal"? You just hid all the complexity in those undefined `convert` and `format` methods.
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...