Python’s new t-strings
481 comments
·April 21, 2025serbuvlad
rwmj
I did a safe OCaml implementation of this about 20 years ago, the latest version being here:
https://github.com/darioteixeira/pgocaml
Note that the variables are safely and correctly interpolated at compile time. And it's type checked across the boundary too, by checking (at compile time) the column types with the live database.
tasuki
Yes, what you did is strictly more powerful than what the Python people did. And you did it 20 years ago. Well done, have an upvote. And yet, here we are in 2025 with Python popularity growing unstoppably and (approximately) no one caring about OCaml (and all the other languages better than Python). It makes me sad.
sanderjd
Network effects are a beast!
But my two cents is that we're pretty lucky it's python that has taken off like a rocket. It's not my favorite language, but there are far worse that it could have been.
rwmj
I'm switching between C, OCaml, Python, bash & Rust roughly equally every day (to a lesser extent, Perl as well). Not everything is what gets on the front page of HN.
skeledrew
It's interesting how the majority has explicitly chosen NOT to use the "better" languages. Is the majority really that bad in their judgment? Or is it that "better" is actually defined by adoption over time?
benwilber0
Aren't there other benefits to server-side parameter binding besides just SQL-injection safety? For instance, using PG's extended protocol (binary) instead of just raw SQL strings. Caching parameterized prepared statements, etc.
Also:
db.execute(t"QUERY WHERE name = {name}")
Is dangerously close to: db.execute(f"QUERY WHERE name = {name}")
A single character difference and now you've just made yourself trivially injectible.I don't think this new format specifier is in any way applicable to SQL queries.
WorldMaker
Templates are a very different duck type from strings and intentionally don't support __str__(). The SQL tool can provide a `safe_execute(Template)` that throws if passed a string and not a Template. You can imagine future libraries that only support Template and drop all functions that accept strings as truly safe query libraries.
> Caching parameterized prepared statements, etc.
Templates give you all the data you need to also build things like cacheable parameterized prepared statements. For DB engines that support named parameters you can even get the interpolation expression to auto-name parameters (get the string "name" from your example as the name of the variable filling the slot) for additional debugging/sometimes caching benefits.
kazinator
But t"..." and f"..." have different types; we can make db.execute reject character strings and take only template objects.
HackerThemAll
Yeah that would be a backward compatible way to do stuff.
hombre_fatal
You solve that with an execute(stmt) function that requires you to pass in a template.
In Javascript, sql`where id = ${id}` is dangerously close to normal string interpolation `where id = ${id}`, and db libs that offer a sql tag have query(stmt) fns that reject strings.
zahlman
> A single character difference and now you've just made yourself trivially injectible.
No; a single character difference and now you get a `TypeError`, which hopefully the library has made more informative by predicting this common misuse pattern.
masklinn
> Aren't there other benefits to server-side parameter binding besides just SQL-injection safety? For instance, using PG's extended protocol (binary) instead of just raw SQL strings. Caching parameterized prepared statements, etc.
All of which can be implemented on top of template strings.
> A single character difference and now you've just made yourself trivially injectible.
It's not just a one character difference, it's a different type. So `db.execute` can reject strings both statically and dynamically.
> I don't think
Definitely true.
> this new format specifier is in any way applicable to SQL queries.
It's literally one of PEP 750's motivations.
tczMUFlmoNk
> > I don't think
> Definitely true.
The rest of your comment is valuable, but this is just mean-spirited and unnecessary.
willcipriano
from string.templatelib import Template
def execute(query: Template)
Should allow for static analysis to prevent this issue if you run mypy as part of your pr process.That would be in addition to doing any runtime checks.
woodrowbarlow
nitpicking:
> It's not just a one character difference, it's a different type. So `db.execute` can reject strings both statically and dynamically.
in this case, that's not actually helpful because SQL statements don't need to have parameters, so db.execute will always need to accept a string.
davepeck
> Caching parameterized prepared statements, etc.
I didn’t explicitly mention this in my post but, yes, the Template type is designed with caching in mind. In particular, the .strings tuple is likely to be useful as a cache key in many cases.
rangerelf
>> I don't think >Definitely true.
I thought we left middle-school playground tactics behind.
null
VWWHFSfQ
> It's literally one of PEP 750's motivations.
Python is notorious for misguided motivations. We're not "appealing to authority" here. We're free to point out when things are goofy.
null
null
VWWHFSfQ
> I don't think this new format specifier is in any way applicable to SQL queries.
Agree. And the mere presence of such a feature will trigger endless foot-gunning across the Python database ecosystem.
tetha
Or you could use this in a library like sh with
sh(t"stat {some_file}")
With t-strings you could run proper escaping over the contents of `some_file` before passing it to a shell.I'd have to take a look at the order things happen in shell, but you might even be able to increase security/foot-gun-potential a little bit here by turning this into something like `stat "$( base64 -d [base64 encoded content of some_file] )"`.
nhumrich
You should check out PEP 787
zahlman
Oh! I missed this one because I've been looking specifically at the Packaging forum rather than the PEPs forum. This looks like a brilliant use case. (I'm aiming for wide compatibility - back to 3.6 - with my current projects, but I look forward to trying this out if and when it's accepted and implemented.)
Now if only the overall `subprocess` interface weren't so complex....
pauleveritt
We really should just point most of these comments at that PEP. Thanks for getting it out so fast.
Flimm
PEP 787 – Safer subprocess usage using t-strings https://peps.python.org/pep-0787/
tetha
Hmm, PEP-787 has some interesting discussions around it. I'll have to sort my thoughts on these aspects a bit.
dhruvrajvanshi
Not Python but this is exactly the idea behind zx
mikeholler
A potential concern is how close this looks to the pattern they're trying to override.
db.execute(f"QUERY WHERE name = {name}")
versus db.execute(t"QUERY WHERE name = {name}")
notatoad
The key point is that t-strings are not strings. Db.execute(t”…”) would throw an exception, because t”…” is not a string and cannot be interpreted as one.
In order for a library to accept t-strings, they need to make a new function. Or else change the behavior and method signature of an old function, which I guess they could do but any sanely designed library doesn’t do.
Handling t-strings will require new functions to be added to libraries.
gls2ro
yes but the bug is writing f instead of t and I assume f will just work.
To clarify even more:
The problem is not writing by mistake t instead of f => this is what we want and then for this we implement a new function
The problem is writing f instead of t => and this will silently work I assume (not a Python dev just trying to understand the language design)
fzzzy
But won't the f string version fail loudly because there's no name parameter?
benwilber0
the {name} parameter is in the locals() dict like it always is
Tenoke
I don't see what it adds over f-string in that example?
ds_
The execute function can recognize it as a t-string and prevent SQL injection if the name is coming from user input. f-strings immediately evaluate to a string, whereas t-strings evaluate to a template object which requires further processing to turn it into a string.
Tenoke
Then the useful part is the extra execute function you have to write (it's not just a substitute like in the comment) and an extra function can confirm the safety of a value going into a f-string just as well.
I get the general case, but even then it seems like an implicit anti-pattern over doing db.execute(f"QUERY WHERE name = {safe(name)}")
teruakohatu
If I pass an f-string to a method, it just sees a string. If I pass a t-string the method can decide how to process the t-string.
sureglymop
Wouldn't this precisely lead to sql injection vulnerabilities with f-strings here?
burky
f-strings won’t sanitize the value, so it’s not safe. The article talks about this.
Tenoke
The article talked about it but the example here just assumes they'll be there.
sim7c00
it makes it so people too lazy to make good types and class will be getting closer to sane code without doing sane code...
imagine writing a SqL where u put user input into query string directly.
now remember its 2025, lie down try not to cry.
evertedsphere
safety against sql injection
zahlman
3. It prevents the developer from trying
db.execute(f"QUERY WHERE name = {name}")
or db.execute("QUERY WHERE name = %s" % name, ())
or other ways of manually interpolating the string - because `db.execute` can flag a `TypeError` if given a string (no matter how it was constructed) rather than a `Template` instance.int_19h
Python is not the first one to get this feature. It's been present in JS for some time now, and before that in C# (not sure if that's the origin or they also borrowed it from somewhere). Python adopted it based in part on successful experience in those other languages.
serbuvlad
That's really cool. I don't use JS or C#, so I wasn't aware of this, but it's a good idea.
NewEntryHN
Assuming you also need to format non-values in the SQL (e.g. column names), how does the `execute` function is supposed to make the difference between stuff that should be formatted in the string vs a parametrized value?
masklinn
Same as currently: the library provides some sort of `Identifier` wrapper you can apply to those.
NewEntryHN
Fair enough. It would be nice if Python allowed to customize the formatting options after `:`
This way you could encode such identifier directly in the t-string variable rather than with some "out-of-band" logic.
florbnit
> In addition, I hope that the tooling ecosystem will adapt to support t-strings. For instance, I’d love to see black and ruff format t-string contents, and vscode color those contents, if they’re a common type like HTML or SQL.
This is such a strange take on t-strings. The only way for anything to infer that the template string is supposed to turn into valid HTML or SQL is to base it of the apparent syntax in the string, which can only be done in an ad-hoc fashion and has nothing to do with the template string feature.
The way the feature has been designed there is no indication in the string itself what type of content it is or what it will eventually be converted to. It’s all handled by the converting function.
As others have added, something like sql”select * from {table}” would have been able to do this, but there’s not even any guarantees that something that is in a template that will be converted into valid sql by a converting function should be any type of valid sql prior to that conversion. For all you know t“give me {table} but only {columns}” might be a converted into valid sql after the template is processed.
davepeck
> This is such a strange take on t-strings
I understand why this seems strange at first!
As Paul mentioned, we spent quite a lot of time considering these issues as PEP 750 came together. In the end, we concluded (a) the PEP leaves open quite a few potential approaches for tools to adopt (not just the one you suggest, as others here have pointed out), and (b) it's ultimately something that the broader tooling community needs to rally around and should probably be out of scope for the PEP itself.
So, with that background in mind, I am indeed hopeful we'll see the ecosystem adapt! :-)
florbnit
I get that. But that “ecosystem might adapt” has absolutely nothing to do with t-strings is what I’m saying. Anyone who wants to validate sql will have to just look through strings guess that it’s sql and validate and highlight it. This could be done before and after just the same. The template strings by nature of just being “strings that will turn into something else at some later point” do nothing to further this in any way.
spankalee
I think you'll just see a pattern like:
html(t"<h1>Hello</h1>")
And highlighters and static analyzers will key off of this.JavaScript's tagged template literals are actually about as flexible as this, since you can dynamically choose the tag function, it's just very rare to do so, so tools assume a lot based on the name of the function. Python tools can basically do the same thing, and just not support t-strings that aren't nested inside a well-named processing function.
florbnit
> And highlighters and static analyzers will key off of this.
Then the t is redundant and we don’t need t strings involved in any way. This would work just as well. In fact better by using html("<h1>Hello</h1>") and have it return a html object instead of returning a just a string which is what t-strings will do. So literally the only contribution from t-strings in that context is to make things worse.
If they had just standardized some way of putting semantics into the template everything would be better. Let us define a “sql = temlate(….)” and use sql”…”
dandellion
Another option would be type hints, something like `title: HTMLTemplate = t"<h1>Hello</h1>"`.
pauleveritt
The original PEP and the original discussion had this in scope. We removed it to let this emerge later. There are different ways to signal the language -- some more friendly to tooling, some more robust.
lolinder
PyCharm (as well as the other JetBrains IDEs) has for at least 10 years supported language injection in Python strings using a comment [0]. Worst case scenario there's no reason whatsoever that that couldn't be used by formatters to apply auto-formatting in a structured and deterministic way.
But that's far from the only option, either! IntelliJ for Java also supports annotations on arguments [1] that then mean that you get syntax highlighting everywhere you use a string literal with the given function:
public void query(@Language("SQL") String sql)
In Python, typing.Annotated appears to have been specifically designed for purposes like this [2]:> If a library or tool encounters an annotation Annotated[T, x] and has no special logic for the metadata, it should ignore the metadata and simply treat the annotation as T. As such, Annotated can be useful for code that wants to use annotations for purposes outside Python’s static typing system.
So something like this should be perfectly viable:
SQL = Annotated[Template, "language", "SQL"]
def query(sql_query: SQL):
# do stuff with Template to sanitize
query(t"SELECT * FROM foo WHERE bar={bar}")
Now, where you're right is that we shouldn't actually require template strings to accomplish this! As noted, JetBrains has been doing this since forever. But maybe template strings will be useful enough for purposes like this that the tooling will actually evolve to support it in formatters and other editors (and maybe PyCharm can get some of the better support that Java has from JetBrains).[0] https://www.jetbrains.com/help/pycharm/using-language-inject...
[1] https://www.jetbrains.com/help/idea/using-language-injection...
[2] https://docs.python.org/3/library/typing.html#typing.Annotat...
pauleveritt
I'm one of the PEP authors and also with JetBrains. I'm talking to the PyCharm team about this.
acdha
Couldn’t you do this with a type annotation? e.g. SQLAlchemy could have a SQL type so tools like mypy could see a Template instance and confirm you’re using it safely but Black, Ruff, or SQLFluff could look for the more specialized Annotated[Template, SQL] to realize that the template could be formatted as SQL, and something like Django could even have Annotated[Template, Email], Annotated[Template, HTML], or Annotated[Template, JSX] to indicate what context the same templating syntax is targeting.
pauleveritt
This is what we discussed in the first revision of the PEP (the use of `Annotated`.) But we found out: linters don't know anything about the Python type system.
We hope to get a community around all of this, stuff at PyCon US, EuroPython, etc. and work some of this out. The JSX/TSX world really has good tooling. We can provide that for those that want it, perhaps better on some aspects.
acdha
Interesting, thanks for the background. I’ve been curious what Astral is going to do in the space but also worry about what happens when their funding runs out.
Someone
> The only way for anything to infer that the template string is supposed to turn into valid HTML or SQL is to base it of the apparent syntax in the string
Not the only thing. You can also look at how it is used. Your editor could know of how some popular libraries use t-strings, track which t-strings get passed into functions from those libraries, and use that to assume what grammar the t-string should follow.
Is that cheating? In some sense, yes, but it also is useful and likely will be worth it for quite a few programmers.
florbnit
If you replace each mention of t-strings in your post with strings or f-strings it’s still true. So that means t-strings add nothing in that regard.
pphysch
Python has had type annotations for a decade, and modern IDEs can interpret them.
Writing `query: SQL = t"SELECT ..."` is a small price to pay for such a DX boost.
florbnit
I mean yes, so t-strings add nothing.
You could already do everything you are proposing t-strings will make easier with just strings or f-strings.
‘query: SQL “Select …”’
“Wait” you’ll say “then the function taking the string has to parse the string and will have to add ugly logic grabbing variables from globals()!” Ah yes. That’s such an ugly solution… let’s instead codify just that as the official best practice and sell people on it by prefixing a t to the string so they don’t notice…
TekMol
Will this allow neat SQL syntax like the following?
city = 'London'
min_age = 21
# Find all users in London who are 21 or older:
users = db.get(t'
SELECT * FROM users
WHERE city={city} AND age>{min_age}
')
If the db.get() function accepts a template, it should, right?This would be the nicest way to use SQL I have seen yet.
meander_water
This definitely seems like one of the motivations for implementing this feature in the first place - https://peps.python.org/pep-0750/#motivation.
Having more control over the interpolation of string values is a win IMO.
zahlman
You would need triple-quotes to span multiple lines, but yes, that is exactly how it's intended to work. `db.get` will receive a Template instance, which stores string parts something like `('\n SELECT * FROM users\n WHERE city=', ' AND age>', '\n')` and interpolated parts like `(Interpolation('London'), Interpolation(21))`. It's then responsible for assembling and executing the query from that.
fweimer
The SQLite extension for Tcl offers something similar:
db1 eval {INSERT INTO t1 VALUES(5,$bigstring)}
https://sqlite.org/tclsqlite.html#the_eval_methodwizzwizz4
As I understand, that's less powerful, because you can do:
t"INSERT INTO mytable VALUES ({s}, {s[::-1]})"
but you can't do: mydb eval {INSERT INTO mytable VALUES ($s, [string reverse $s])}
Instead, you have to write: set t [string reverse $s]
mydb eval {INSERT INTO mytable VALUES ($s, $t)}
There's no reason you couldn't have such power in Tcl, though: it's just that the authors of SQLite didn't.mcintyre1994
That’s the sort of thing people have built with the equivalent feature in JavaScript, so it should do. Eg https://github.com/andywer/squid is a nice example.
bombela
Yes, it's quite delightful.
zelphirkalt
Isn't the actually proper way to use prepared statements anyway? If we are doing that properly, then what does this t string business buy us for SQL usage from Python?
scott_w
Because, as the article states, people aren’t using prepared statements. Instead, they pass f-strings because they’re more convenient.
vultour
Except to maintain backwards compatibility we're probably going to get new methods that only accept templates, completely circumventing any effort to stop people passing in strings.
Prepared statements were the recommended way to run SQL queries when I was starting with PHP 15 years ago, anyone writing code vulnerable to SQL injection at this point should not be writing code.
hedgehog
f strings are syntax rather than a type, the resulting templates look like a reasonable way to specify a prepared statement.
neonsunset
> This would be the nicest way to use SQL I have seen yet.
EF/EF Core has existed for years :)
https://learn.microsoft.com/en-us/ef/core/querying/sql-queri...
nurettin
I've used it for years. In order to generate the models you had to use the visual designer which was slow and buggy.
Generally annoying experience if you have to clock in and out every day to watch that UI break your database relations whenever you click save.
neonsunset
No one uses it today, or in the last 5 years or so I presume. You use https://learn.microsoft.com/en-us/ef/core/modeling/#use-data...
This was a completely separate, legacy extension of VS, not EF let alone EF Core.
jbaiter
Thanks, I hate it. While it's nice syntactic sugar, the difference between an SQL injection vulnerability and a properly parametrized query is now a single letter that's easily missed
JimDabell
The t-string produces a Template object without a __str__() method. You can’t mistakenly use an f-string in its place. Either the code expects a string, in which case passing it a Template would blow it up, or the code expects a Template, in which case passing it a string would blow it up.
crazygringo
> or the code expects a Template, in which case passing it a string would blow it up.
That's where the problem is though -- in most cases it probably won't blow up.
Plenty of SQL queries don't have any parameters at all. You're just getting the number of rows in a table or something. A raw string is perfectly fine.
Will sqlite3 really disallow strings? Will it force you to use templates, even when the template doesn't contain any parameters?
You can argue it should, but that's not being very friendly with inputs, and will break backwards compatibility. Maybe if there's a flag you can set in the module to enable that strict behavior though, with the idea that in a decade it will become the default?
politelemon
And I'm guessing lots of code will expect strings to maintain backward compatibility.
b3orn
If it's not a completely new library written exclusively around templates, such code currently accepts strings and will most likely continue to accept strings for backwards compatibility.
yk
That's entirely implementation dependent. For existing libraries I would expect something like
def get(self, query):
if isinstance(query, template):
self.get_template(query)
else:
self.get_old(query) #Don't break old code!
solatic
That would be a great argument if Python wasn't a language that let you reach into the internals and define __str__() for things you shouldn't be defining it for. And that is something people will definitely do because, you know, they just need something to friggin work so they can get whatever ticket closed and keep some metric happy tracking time-to-close
baggiponte
Type checkers to the rescue ahaha I think db.get could also raise if the type does not match?
TekMol
I guess that is a misunderstanding on your side, about how templates work. Less hate and more love might help to avoid this type of hotheaded misconception ;-)
Why do you think changing a letter would cause a vulnerability? Which letter do you mean?
codesnik
f'' vs t'' probably.
hyperbovine
OP is referring to swapping t with f.
null
yxhuvud
Also I wonder how easy it will be to shoot oneself in the foot. It may be easy to accidentally make it to a string too soon and not get the proper escapeing.
scott_w
That’s a library author problem, so it’s less likely since library authors tend to be fewer in number and, for popular libraries, get a reasonable number of eyes on this type of change.
nu11ptr
Personally, this feels like a feature that is too focused on one problem to be a general feature. Python is getting huge. When people ask me if Python is easy and simple to learn I have to say "the basics, yes, but to to learn the whole language... not so much".
I feel like in this sense Go really is interesting by rejecting almost every single feature. Honestly not sure generics were worth it as they add a lot of complexity, and while they are nice, I don't need them very much. The general idea to keep the language at its original focus is the right idea IMO. C++ would be the most extreme case where the language itself barely resembles what it started out as.
oliwarner
Python has always been a batteries-included language, so having a go at templated string interpolation —a feature other languages have had for decades— seems like a strange gripe.
It's far more essential than little utilities like textwrap or goliath packages like Python's bundled tkinter implementation.
phreeza
Batteries included to me also meant a large standard library, not a large language.
nu11ptr
What other languages have this feature? I'm not aware of any
thatnerdyguy
C# has InterpolatedStringHandler which isn't quite the same thing, but (in my understanding), trying to address the same core issues.
pansa2
> [T-strings] are the pythonic parallel to JavaScript’s tagged templates.
jerf
Many languages have similar features.
For instance, Python has the % operator that is a template format that allows interpolating values based on a template string with a variety of printf-like features: https://python-reference.readthedocs.io/en/latest/docs/str/f...
Also, Python has the .format method on strings, which is a template format that allows interpolating values based on a template string: https://www.geeksforgeeks.org/python-string-format-method/
As another example, Python has f-strings that are a template format that allows interpolating values based on a template string: https://www.geeksforgeeks.org/formatted-string-literals-f-st...
Also, you can also find languages like Python that have a rich ecosystem of third party templating solutions. These are often intended for things like rendering entire web pages but many of them have relatively simple ways of using their templating functionality in a fairly reasonable amount of code, if you just want to have a template format that allows interpolating values based on a template string.
So, as you can see, many other languages have this feature, as you can tell from all the examples I have shown you here.
(To spell it out for those who may find this too subtle... somehow... I'm not a fan of this simply because Python has gone from "There should be one-- and preferably only one --obvious way to do it." to "there's half-a-dozen ways to do it and if they are all wrong Python 3.x+1 will introduce a seventh" and I'm just not seeing the benefits worth the tradeoffs here.)
hocuspocus
Scala since 2014.
Java 22 had the feature as preview but it was removed in 23, it'll come back after some refinement.
martinky24
They literally discuss this in the article.
murkt
This is a pretty simple and useful feature. I wouldn’t say that it bloats the language too much. Descriptors and metaclasses are much more complicated and have a lot more implications and have been in the language for a veeeeery long time. Is it decades already?
nu11ptr
This feature is not complicated, but one must keep every feature that can possibly be seen in code in their head. Even if it is familiar now, what happens when you use the feature in the one small section of code where it fits, nowhere else, and then read that code 2 years later? This is the problem with adding useful features that are only used in a few key places. I'm not saying Go is a perfect language, far from it, but limiting # of features as a general goal is something more languages should strive for IMO.
murkt
I am not arguing against that language ideally should have less features. I am arguing with “Python is getting huge”, because it’s huge and has been for many-many years :)
pansa2
Yeah, Python hasn’t been a simple language for a long time, if ever. That’s probably the biggest misconception about the language - that its friendly syntax implies simple semantics. It’s not true at all.
tetha
I would say python in it's entirety is one of, if not the deepest and potentially most complex language I know. C++ is the other contender. The things you could do with metaclasses, multiple inheritance and operator overloading are quite staggering.
I'm just glad you don't have to think or even use this as a normal user of the language, most of the time or at all.
kccqzy
A lot of Python's complexity like descriptors and metaclasses and the so-called cooperative multiple inheritance come from an era where object orientation is gospel and yet people keep finding limitations to what can be accomplished in an OO paradigm and therefore they patch up the language to support ever more complicated use cases.
I have a feeling that if metaclasses and descriptors were to be proposed today it would be laughed out of the PEP process completely.
pansa2
> I have a feeling that if metaclasses and descriptors were to be proposed today it would be laughed out of the PEP process completely.
I think you need at least one of these. Modern Python eschews metaclasses but descriptors are everywhere. Without them you couldn’t have @classmethod, @staticmethod nor @property - or at least, you couldn’t generalise custom method lookup and all three of those would need to be special cases.
The alternative approach is to drop descriptors and make method lookup much simpler, but then you need metaclasses (see: static methods in Ruby).
sanderjd
I'm truly glad that Go exists for people who like languages that are simple even to the point of frustration and I hope it never changes. But I'm glad that other languages exist for those of us for whom learning some syntax is not a barrier and having convenient ways to do common things is highly valued.
acdha
There’s an interesting trade off around maintenance. Python’s stdlib means there’s more consistency across projects and you can assume basic things like error handling which you had to check individually on each Go program, which is the kind of stuff which adds up when you have a lot of small programs.
This is especially noticeable with AWS Lambda where you can have a lot of useful stuff running for years without doing more than bumping the runtime version every year or two, but also that is one highly opinionated architecture so it’s not globally optimal for everyone.
gnabgib
Big discussion (414 points, 10 days ago, 324 comments) https://news.ycombinator.com/item?id=43647716
damnitbuilds
Maybe it's good to come back to a discussion after a few days, more informed and with a cooler head ?
;-)
runekaagaard
It feels a bit like "cheating" that new x-string features are built-in only. It would be cool to be able to do:
from foo import bar
bar"zoop"
masklinn
A t-string is a literal for a Template object which is a data holder, it doesn't actually do anything, so you would simply call
bar(t"zoop")
cb321
This is exactly how Nim is. The f-string like equivalent uses a macro called "fmt" which has a short alias "&". So you can say:
import std/strformat
let world = "planet"
echo &"hello {world}"
The regular expression module does a similar thing with a `re"regular expression"` syntax or std/pegs with peg"parsing expression grammar" and so on. There are probably numerous other examples.In general, with user-defined operators and templates and macros, Nim has all kinds of Lisp-like facilities to extend the language, but with a more static focus which helps for both correctness and performance.
nhumrich
This was the original proposed idea in the PEP (750), but it changed overtime. There is a section in the PEP to explain why it changed to t-strings if you are interested.
zahlman
PEP 638 has always seemed to me like something of a generalization of the idea. But that really feels to me like a 4.0 feature, or rather something that needs to be designed for from the beginning. (Which is why I've done a bit of that in my own mind...)
Timwi
True. Then you could use
sql"..."
html"..."
for each of the given examples and achieve some runtime type safety.jbverschoor
And you'd end up with almost no improvement.
If you pass a "t-string" to a framework, it can force escaping.
What you suggest is to rely on escaping by the user (dev), who, if he was aware, would already escape.
Unless you'd suggest that it would still return a template, but tagged with a language.
Timwi
No, you misunderstood that completely. They would still be like t-strings, but sql-strings are now a different type from html-strings. The escaping would be done by the library that offers the sql"..." functionality.
mcintyre1994
FWIW the JS equivalent is a template but tagged with a language. It has all the benefits of this template, but IDEs can easily syntax highlight the string. That seems like it would be a bit trickier to do with the Python one which is a shame.
thund
Use a function?
bar(“zoop”)
It’s just syntax, like we used to have print “foo”
that later became print(“foo”)
zahlman
In my own language design (nothing public yet - need to prioritize more practical things at the moment) this is definitely on the menu. Aside from a minimal set, keywords are implemented as compile-time macros; and it's intended that they can be extended, both "natively" and in the implementation language, by writing AST-manipulation code. But the handling of arithmetic expressions, as well as the broader line/block structure, is hard-coded. (I draw inspiration from both Python and the Lisp family.)
dec0dedab0de
meh, the difference between bar"zoop" and bar("zoop") isn't really big enough to be worth it.
I like F strings a lot, but for the most part I think all of the various X-strings should just be classes that take a string as an argument.
mounir9912
What I really don't get is how it's any different than applying whatever function you would apply to the template, on the f-string variables. So instead of:
evil = "<script>alert('bad')</script>"
template = t"{evil}"
safe = html(template)
Why not just: evil = "<script>alert('bad')</script>"
safe = f"{html(evil)}"
Or even before creating the f-string. Is it just about not forgetting the sanitization/string manipulation part and forcing you to go through that?zahlman
> Is it just about not forgetting the sanitization/string manipulation part and forcing you to go through that?
This is a very big deal! It's also about centralizing that work. Now that sanitization can occur in the consumer of the t-string (for example, the API to your HTML renderer), rather than in every f-string.
scott_w
Pretty much, yeah. The article highlights that people were using f-strings directly, and they wanted to provide an alternative for lightweight template/interpolation.
mounir9912
I feel like I'm still missing something when they're saying this about the example(s):
"Neither of these examples is possible with f-strings. By providing a mechanism to intercept and transform interpolated values, template strings enable a wide range of string processing use cases."
As far as I can see, anything you do with the template, you could do before building the f-string or inline as in my intial example.
davepeck
With f-strings, you cannot write code to determine which portions of the resulting `str` were static and which were dynamic; with t-strings, you can. *
(As to your initial example: it's worth considering what will happen as you compose multiple bits of HTML via nesting to generate a large final page. The developer experience may become... unideal.)
* (barring undesirable hacks with inspect, etc.)
scott_w
You wouldn’t really do your example, though. If you’re using an f-string, you’d just directly interpolate, because it’s convenient. You wouldn’t use an extra library to properly make it safe, otherwise you’d just use a proper template library and language.
This gives you a convenient middle ground where you don’t need to learn a template library but still get safety. I can’t think of the code right now but I could see this being useful to pass in some dynamic HTML to, say, Django without having to remember to turn off escaping for that section. It can also be convenient for writing raw SQL without having to use prepared strings.
nikisweeting
This is pretty cool, if we're porting JS features do we get dictionary unpacking/destructuring next?
I just want this so badly, it's the main reason I drift back to JS:
>>> {a, b=45, c=None, **d} = {'a': 234, xzy: 32456}
>>> print(a, b, c, d)
234 45 None {'xyz': 32456}
zahlman
Another alternative:
>>> a, b, c, d = (lambda a, b=45, c=None, **d: (a, b, c, d))(**{'a': 234, 'xyz': 32456})
The parentheses for this are admittedly a bit tricky.enragedcacti
just use this very sane pattern /s:
>>> match {'b': 45, 'c': None}|{'a': 234, 'xyz': 32456}:
>>> case {'a': a, 'b': b, 'c': c, **d}: pass
>>> print(a, b, c, d)
234 45 None {'xyz': 32456}
sashank_1509
Why does this need to be a language feature. This could just be a separate library, we could use brackets instead of a letter before a string. I fear, Python is going down the path of C++
jsnell
It being a language feature gives you controlled access to the lexical scope, such that the template string can refer to variables by name rather than having to pass each value as a parameter. Doing it via parameters is repetitive and/or error-prone.
linsomniac
You CAN get access to the calling scope of a function. Something like:
current_frame = inspect.currentframe()
env = current_frame.f_back.f_locals.copy()
I did it in uplaybook so that you can do things like: for module in ['foo', 'bar', 'baz']:
ln(path="/etc/apache2/mods-enabled", src="/etc/apache2/mods-available/{{ module }}.load")
This is an ansible-like tooling with a python instead of YAML syntax, hence the Jinja2 templating.nhumrich
This feature actually can't be a library. It needs to be able to preserve the string before the variables are passed in, which a library would be unable to do because f-strings immediately replace all the values. The whole premise of t-strings is to know which values where "hard coded" and which ones are variables. A library can't possibly know that. And a function call wouldn't have access to the local variables. The only way to do this without language support is to pass `locals()` into every function call.
dec0dedab0de
Now you have me wondering how difficult it would be to have a class that takes a string and parses through globals to find the context it was called from. maybe causing an exception and abusing a traceback? or maybe we just find our own objectid.... gahh I have to try it now, but I'm setting a timer.
dec0dedab0de
It was easier than I thought, but also took longer than I wanted to. turns out the inspect module provides what you need to pull it off.
This dummy example splits a string that it was given, then if one of those values is in the callers context it saves those in self.context, and has an output function to assemble it all together. Obviously this example is not very useful, but it shows how a library could do this as a class or function without the user having to pass in locals().
import inspect
class MyXString:
""" will split on whitespace and replace any string that is a variable name
with the result of str(variable)"""
def __init__(self, string):
self.string = string
caller_locals = inspect.currentframe().f_back.f_locals
self.context = {}
for key in set(string.split()):
if key in caller_locals:
self.context[key] = caller_locals[key]
def output(self):
output = self.string
for k,v in self.context.items():
output = output.replace(k,str(v))
return output
zephyrfalcon
You don't have to go through all the globals, you just have to get the caller's namespace, which is fairly simple. See e.g. https://stackoverflow.com/a/6618825/27426
For this reason, I think it's not true that this absolutely had to be a language feature rather than a library. A template class written in pure Python could have done the same lookup in its __init__.
Mawr
It's not as simple as "more features" == "closer to C++". Features are not equal to each other in terms of impact on language complexity.
t-strings don't interact with anything else in the language; they, as you yourself pointed out, could almost be an isolated library. That makes them low impact.
This is also true syntactically; they're just another type of string, denoted by "t" instead of "f". That's easy to fit into one's existing model of the language.
Moreover, even semantically, from the point of view of most language users, they are equivalent to f-strings in every way, so there's nothing to learn, really. It's only the library writers who need to learn about them.
Then we have to consider the upsides - the potential to eliminate SQL and HTML injection attacks. The value/cost is so high the feature a no-brainer.
perlgeek
If it's not a language feature, there's always a risk of fragmentation. Some people won't use it because it adds another dependency, that means fewer programmers will be familiar with it. Others will come up with their own, slightly incompatible implementation. See for example Perl and its various Object Orientation frameworks (Moose, Mouse, Moo, Mo, M, Mojolicious comes with its own...)
Other languages have a policy of prototyping such things out of core, and only adding it to the core language if it gains traction. Of course that works better if the language has a mechanism for extending the syntax out of core.
dec0dedab0de
putting it in the standard library would handle those issues.
sanderjd
Counterpoint: It's good to add well designed and useful features to programming languages.
dsego
Because JS has it in the form of tagged template literals.
chrisrodrigue
We have a handful of ways to create strings now.
Meanwhile, pytest is still not part of the standard library.
metrognome
Zen of Python in 2025:
There should be one-- and preferably only one --obvious way to do it.
Python String Formatting in 2025:- t-strings
- f-strings
- %-operator
- +-operator
- str.format()
davepeck
And $-strings (PEP 292) as well. :-)
I see each of these as distinct but overlapping; I'm (slowly) writing a guide to string formatting with all of these in mind, trying to emphasize when I'd choose one over the other. (fwiw I personally avoid % and + these days; $ is pretty uncommon in practice; f-, t-, and .format() all seem to have good unique uses.)
a_t48
The last three generally shouldn't be used (`+` is sometimes fine, but not really for formatting), but I doubt we would ever get a py4 that removes them, given the stomachaches that py3 caused. It does feel odd that a t-string is just an f-string without args applied.
alfalfasprout
It's my understanding that they're still recommended for logging since with f-strings you always pay the formatting cost (but with str.format it's deferred).
sanderjd
t-strings aren't really like these others.
It's definitely true that those four string formatting techniques violate the "one obvious way" advice.
hughw
I could scarcely believe this new t-string wasn't a joke. As an occasional, grudging Python programmer, I rue the accretion of string formatters over the years. My code bears the history of successive, imperfectly understood (by me)formatters.
"Situation: There are 14 competing standards...." https://xkcd.com/927/
nezirus
Maybe this could be useful to libraries like psycopg3 to use something more simple/natural instead of this:
https://www.psycopg.org/psycopg3/docs/api/sql.html
(while I also agree it gets crowded with yet another string prefix)
dtech
That's the exact use case. Basically these are syntactic sugar for a very similar function signature.
davepeck
Hi! I wrote this. :-)
I'm a little late to the conversation (and a bit surprised to see this trending on HN) but am happy to answer any questions; I'll try to pop in throughout the day.
maxloh
Hi. I come from a JavaScript background.
I am wondering what is the reason behind not using a similar syntax to JavaScript? Seems simpler to me.
# Compare this:
template = t"<p>{evil}</p>"
safe = html(template)
# To this:
safe = html"<p>{evil}</p>"
davepeck
The PEP originally started with a similar-to-javascript syntax but over time we decided it wasn't the right way to expose these ideas in Python. There's more detail about why this approach was rejected in the PEP: https://peps.python.org/pep-0750/#arbitrary-string-literal-p...
varunneal
Would be interested in inclusion of PEP 292 [1] in your discussion here, which introduced `string.Template`. Is this Template going to be deprecated?
davepeck
PEP 292's `string.Template` will remain; there are no plans to deprecate it.
PEP 750's `string.templatelib.Template` is a separate and unrelated type. Amongst many differences, unlike PEP 292, `Template` has a literal form too.
I'm hopeful that the confusion will be minimal; in practice, PEP 292 (aka $-strings) is used only in specialized cases, like flufl.i18n, a really deep I18N framework.
18172828286177
This is super cool, thank you.
All things considered, this is pretty cool. Basically, this replaces
with Does the benefit from this syntactic sugar outweigh the added complexity of a new language feature? I think it does in this case for two reasons:1. Allowing library developers to do whatever they want with {} expansions is a good thing, and will probably spawn some good uses.
2. Generalizing template syntax across a language, so that all libraries solve this problem in the same way, is probably a good thing.