db.execute("QUERY WHERE name = ?", (name,))
with db.execute(t"QUERY WHERE name = {name}")
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.
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.
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.
python is popular because its what teachers teach.
Also, in 2025, just use uv.
gp's claim is not "its easy to learn". It's not just the concept -- it's the ergonomics, absolutely terrible footguns (especially when dealing with global wheels that can screw up your running system), and the hidden state.
You don't need to `import` anything to start teaching Python. Even then you can do quite a lot with the standard library. Even then, unless you're using 3.11 or later on Linux you can let Pip install with `--user` until you actually need to isolate things between projects. (And even with new Python on Linux, the instructor can typically avert this by just installing a separate Python in `/usr/local/bin` for example. Yes, that's "cheating", depending on the classroom environment. But that's part of the point: installation hurdles are hurdles for self-learners, not for students.)
You only need to learn about virtual environments once you have projects with mutually conflicting dependencies, and/or once you're at a point where you're ready to publish your own software and should be learning proper testing and development practices. (Which will be largely orthogonal to programming, and not trivial, in any language.)
And when your students do get to that point, you can give them a link such as https://chriswarrick.com/blog/2018/09/04/python-virtual-envi... .
Teachers teach Python because it's easy to teach while still being relevant to the real world, in particular because boilerplate is minimized. You don't have to explain jargon-y keywords like "public" or "static" up front. You don't have to use classes for quite some time (if ever, really). You can express iteration naturally. Types are naturally thought of in terms of capabilities.
In my mind, Python has all the pedagogical advantages of Lisp, plus enough syntactic cues to prevent getting "lost in a sea of parentheses". (Of course, it lacks plenty of other nice Lisp-family features.)
What you say here reminds me of something Peter Norvig said 15 years ago on this site: https://news.ycombinator.com/item?id=1803815
> Peter Norvig here. I came to Python not because I thought it was a better/acceptable/pragmatic Lisp, but because it was better pseudocode. Several students claimed that they had a hard time mapping from the pseudocode in my AI textbook to the Lisp code that Russell and I had online. So I looked for the language that was most like our pseudocode, and found that Python was the best match. Then I had to teach myself enough Python to implement the examples from the textbook. I found that Python was very nice for certain types of small problems, and had the libraries I needed to integrate with lots of other stuff, at Google and elsewhere on the net.
Basically, that it's better pedagogically because it looks like pseudo-code and it's easy to get up and running quickly.
If we used that logic elsewhere in life we’d all be playing the flute and cycling around on tricycles and balance bikes. But for some reason in tech it’s all about Hello World.
patently not true. you dont get too far into python -- especially if you are reading (or copypastaing) other People's code -- before you see if __name__ == "__main__" and any potential future programmer will rightfully ask "what the absolute fuck is this"
even "def" is kind of a weird fucking keyword.
Don't get me started about teaching beginners which datatypes are pass by reference and which are pass by value.
try explaining to an elementary school student why
def foo(a):
a = a + 1
doesn't change the caller's variable but def bar(a):
a.append(1)
does.If they are beginners to programming, you wouldn't teach them those terms in the context of Python, because neither of those terms map to Python argument passing; Python has one form of argument passing, and it doesn't map closely to the intuition that experienced programmers in languages that have pass by reference and pass by value have about those things. Likewise, the only thing you'd teach someone new to Python that is experienced in languages where those terms are useful is that that distinction is irrelevant in Python, which is pass by assignment (sometimes also called pass by object reference, but pass by assignment is a much more useful description IMO, because argument passing works exactly like assignment to a new variable name.)
> try explaining to an elementary school student why
def foo(a):
a = a + 1
> doesn't change the caller's variable but def bar(a):
a.append(1)
> does.But, that's easy, if you've first taught them how variables, assignment, and mutation work in Python without getting function calls in the way, because it is exactly the same as this
a = 1
b = a
b = a + 1
print(f"{a=}, {b=}")
vs. a = [1]
b = a
b.append[1]
print(f"{a=}, {b=}")
Argument passing is just assignment to a new variable that exists in the scope of the function. Methods which mutate an object affect the object no matter what variable you access it from, assignment operations affect only the variable they assign to. That's exactly the same behavior in one scope as it is between the function and caller scopes.And this distinction has nothing to do with data types, but only with the operations performed (the only connection to data types is that immutable types have no mutation operations in the first place.) You can tell its not about data types because you can use the same types as the second excerpt, and the operations of the first, and get the same results as the first (which shares operations) and not the second (which shares datatypes):
a = [1]
b = a
b = b + [1]
print(f"{a=}, {b=}")
If you understand how assignment and mutation works in one scope, you understand how argument passing works. Trying to teach a distinction that exists between how different operations affect variables that initially reference the same object as a distinction about how datatypes are passed as arguments is confusing, because you as a teacher are presenting the source of the difference in behavior as originating in a completely different place than where it actually comes from. That's not a problem with Python being complex, it is a problem with you taking a very simple thing and making it complex by ascribing it to a source that is completely irrelevant to what is actually going on.First off, if you are teaching someone, you are showing that person the code, and not allowing copy-and-paste.
Second, no, that comes up much less often than you'd expect.
Third, it's the same as `if value == 'example':`. Underscores are not jargon.
Fourth, it's trivially searchable. That's the part where you can copy and paste - into a search engine, which will immediately find you several competent explanations such as https://stackoverflow.com/questions/419163 .
> even "def" is kind of a weird fucking keyword.
Admittedly a poor choice, but not a deal breaker. You need the concept of functions to do programming. But you don't need the concept of data hiding, nor do you need any of the multiple, barely-related concepts described by the term "static".
> Don't get me started about teaching beginners which datatypes are pass by reference and which are pass by value.
There's nothing to explain. They are all pass by value, per the strict meaning of those terms.
Those terms have been widely seen as less than ideal for decades, however, because they fail to account for variables with reference semantics (i.e., what Python uses - which are sometimes called "names"). A more modern term is "pass by assignment", which correctly describes all variable passing in Python: passing an argument to a parameter is a form of assignment, and works the same way as assigning a value to a name.
This is far less complex than C#, in which user-defined types may have either value semantics or reference semantics, and which supports both pass by assignment and two separate, true forms of pass by reference (for initialization and for modifying an existing object). And certainly it's less complex than whatever C++ is doing (see e.g. https://langdev.stackexchange.com/questions/3798 ).
> try explaining to an elementary school student why
First: if someone gives you a bag with three apples in it, you can put another apple in the bag and give it back, and the other person will have a bag with four apples in it. But if you add 3 + 1, that doesn't change the meaning of 3. These are simple ideas that an elementary school student already understands.
Second: from extensive experience teaching beginners (never mind that you are moving the goalposts now), it makes no sense to get into the theory. It's not helpful. A student who can ask about this has already lost the plot, because the two examples use completely different syntax (a method call versus an assignment) so they shouldn't be expected to work similarly. You avoid this problem by using more precise language early on. "Change" is not an appropriate word here.
Third: you give this example because you think that `bar` (and you imply by your naming that a list is being passed) demonstrates pass by reference. This is simply incorrect. Please read https://nedbatchelder.com/text/names1.html.
Fourth: your use of profanity and the overall tone of your writing suggests that you simply don't like the fact that Python works the way that it does. This is not a good look IMO.
Just for the record, I've been in variations of this discussion countless times. I know what I'm talking about. All links above are in my bookmarks.
Did they try using a search engine? But more to the point, if they don't understand what it is, how did they find out it exists?
> how to get it (venv, conda, pip, uv, uvx, …)
uvx is a command from the same program as uv; venv is not a way to obtain packages; and the choice here isn't a real stumbling block.
> because python arrays are shit
I can't say I've seen many people complain about the standard library `array` module; indeed it doesn't seem like many people are aware it exists in the first place.
If you're talking about lists then they serve a completely different purpose. But your use of profanity suggests to me that you don't have any actual concrete criticism here.
> Then they notice that some other dependency requires a previous python version
Where did this other dependency come from in the first place? How did they get here from a starting point of dissatisfaction with the Python standard library?
> but their python is installed globally and other dependencies were installed for that.
... and that's where actual environment management comes in, yes. Sometimes you have to do that. But this has nothing to do with teaching Python. You have necessarily learned quite a bit by the time this is a real concern, and if you were taught properly then you can self-study everything else you need.
> These are uniquely python-specific problems.
No other languages ever require environment management?
> Lisp doesn’t have those problems
Please tell me about your experience using Qi.
What this usually hits isn’t that managing Python packages is hard in 2025 but that many people do not learn how their operating system works conceptually until the first time they learn to program and it’s easy to conflate that with the first language you learn.
gp's claim was:
> Python is better because it's easy for people to learn,
i believe then we agree: it is not.
Good examples of this are numpy and tensorflow.
Instead they can type python to open a shell and use python to immediately run their file.
It's not, because you can fuck up system/unrelated app python dependencies and in extreme cases have to reinstall OS. Thankfully as developers migrate away from python/adopt strategies like flatpak this is less of a problem.
Other PLs do not have this problem.
Some languages definitely had people gravitate towards them due to being innovative in a given space, but in many of those cases, the comparative advantage was lost to other languages/techs/frameworks that simply failed to gain a market share "equal to their innovative contribution" due to the first comer's advantage.
It's also about how much better.
Beyond a decent enough type system, the advantages start to flatten and other factors start to matter more.
Can't speak too much for python, but as someone who's written large amounts of code in OCaml and Typescript, the strictest compiler options for Typescript are good enough.
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.
> 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.
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.
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.
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.
> Definitely true.
The rest of your comment is valuable, but this is just mean-spirited and unnecessary.
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.
def execute(query: Union[str, Template]):
Maybe because they want their execute function to be backwards compatible, or just because they really do want to allow either raw strings are a template string.I’d consider that an invalid use case:
1. You can create a template string without placeholders.
2. Even if the caller does need to pass in a string (because they’re executing from a file, or t-strings don’t support e.g. facetting) then they can just… wrap the string in a template explicitly.
> 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.
No. A t-string with no placeholders is perfectly fine. You can use that even if you have no parameters.
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.
I thought we left middle-school playground tactics behind.
Python is notorious for misguided motivations. We're not "appealing to authority" here. We're free to point out when things are goofy.
Agree. And the mere presence of such a feature will trigger endless foot-gunning across the Python database ecosystem.
I had to look SEVERAL times at your comment before I noticed one is an F and the other is a T.
This won’t end well. Although I like it conceptually, this few pixel difference in a letter is going to cause major problems down the road.
CS has survived for decades with 1 and 1.0 being completely different types.
db.execute("SELECT foo FROM bar;")
db.execute(f"SELECT foo FROM bar WHERE id = {foo_id};")
db.execute(t"SELECT foo FROM bar WHERE id = {foo_id};")
The first and second look identical to execute() because all it sees is a string. But the second one is wrong, a hard-to-see typo of the third.If f-strings didn't exist there'd be no issue because it could distinguish by type as you say. But we have an incorrect SQL-injection-prone usage here that can't be distinguished by type from the correct plain string usage.
db.execute(t"SELECT foo FROM bar;")
See? No reason to accept strings, it's absolutely fine to always error if a string is passed.There's also simply no hard requirement to overload an `execute` function. We have options beyond "no templates at all" and "execute takes templates and strings", for example by introducing a separate function. Why does perfect have to be the enemy of good here?
https://docs.python.org/3/library/sqlite3.html
https://www.psycopg.org/docs/cursor.html
https://dev.mysql.com/doc/connector-python/en/connector-pyth...
If it is going to reject the currently-accepted unsafe usage, its going to be a major breaking change in any case, so I don't see the problem. I mean, if you are lamenting it can't reject the currently-accepted SQL-interpolated-via-f-string because it can't distinguish it by type from plain strings with no interpolation, you are already saying that you want a major breaking change but are upset because the particular implementation you want is not possible. So you can't turn around and dismiss an alternative solution because it would be a major breaking change, that's what was asked for!
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] )"`.
Now if only the overall `subprocess` interface weren't so complex....
db.execute(f"QUERY WHERE name = {name}")
versus db.execute(t"QUERY WHERE name = {name}")
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.
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)
but it will not. f-strings and t-strings are not compatible types, they will not "just work". not unless somebody changes a library to make it just work. as long as nobody does that, it's not an issue.
In the fullness of time it has no reason to. Even in the worst case scenario where you have to compose the query dynamically in a way t-strings can’t support, you can just instantiate a Template object explicitely.
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)}")
And further, if `safe` just returns a string, you still lose out on the ability for `db.execute` to pass the parameter a different way -- you've lost the information that a variable is being interpolated into the string.
I'd prefer the second, myself.
And you add the safety inside db.safe explicitly instead of implicitly in db.execute.
If you want to be fancy you can also assign name to db.foos inside db.safe to use it later (even in execute).
I think one thing you might be missing is that in the t-string version, `db.execute` is not taking a string; a t-string resolves to an object of a particular type. So it is doing your `db.safe` operation, but automatically.
At least db.safe says what it does, unlike t".
Template is also more powerful/concise in that the stringify function can handle the "formatting" args however it looks.
Note also, that there's no requirement that the template ever become a str to be used.
`db.execute(Template)` and `db.unsafeExecute(str)`
also, you're trusting that the library implementer raises a runtime exception if a string a passed where a template is expected. it's not enough to rely on type-checks/linting. and there is probably going to be a temptation to accept `db.execute(sql: Union[str, Template])` because this is non-breaking, and sql without params doesn't need to be templated - so it's breaking some stuff that doesn't need to be broken.
i'm not saying templates aren't a good step forward, just that they're also susceptible to the same problems we have now if not used correctly.
To illustrate the question further, consider a similar html.safe: f"<a href={html.safe(url)}>{html.safe(desc)</a>" - the two calls to html.safe require completely different escaping, how does it know which to apply?
db.execute("SELECT * FROM table WHERE id = ?", (row_id,))
db.execute(f"QUERY WHERE name = {name}")
db.execute(f"QUERY WHERE name = {safe_html(name)}")
Oops, you're screwed and there is nothing that can detect that. No such issue with a t-string, it cannot be misused.For me it just makes it easier to identify as safe, because it might not be obvious at a glance that an interpolated template string is properly sanitised.
Yes, you could require consumers to explicitly sanitize each parameter before it goes into the f-string, or, because it has the structure of what is fixed and what is parameters, it can do all of that for all parameters when it gets a t-string.
The latter is far more reliable, and you can't do it with an f-string because an f-string after creation is just a static string with no information about construction.
Well, no, the library author writes it. And the library author also gets to detect whether you pass a Template instance as expected, or (erroneously) a string created by whatever formatting method you choose. Having to use `safe(name)` within the f-string loses type information, and risks a greater variety of errors.
Yes, the idea is that by having this in the language, library authors will write these implementations for use cases where they are appropriate.
Using a t-string in a db-execute which is, should be as safe as using external parameters. And using a non-t-string in that context should (eventually) be rejected.
The key point here is that a "t-string" isn't a string at all, it's a new kind of literal that's reusing string syntax to create Template objects. That's what makes this new feature fundamentally different from f-strings. Since it's a new type of object, libraries that accept strings will either have to handle it explicitly or raise a TypeError at runtime.
You might have implemented the t-string to save the value or log it better or something and not even have thought to check or escape anything and definitely not everything (just how people forget to do that elsewhere).
class DB:
def execute(query: Template):
...
It would be weird for the implementation to just concatenate everything in the template together into a string without doing any processing of the template parameters. If you wanted an unprocessed string, you would just have the parameter be a string.You're right that the authors of such libraries could choose to do something different with the template parameter. But none of them will, for normal interface design reasons.
A library author could also write an implementation of a `plus` function on a numerical type that takes another numerical type, and return a string with the two numbers concatenated, rather than adding them together.
But nobody will do that, because libraries with extremely surprising behavior like that won't get used by anybody, and library authors don't want to write useless libraries. This is the same.
db.execute("QUERY WHERE name = ?", (name,))
with db.execute(t"QUERY WHERE name = {name}")
It's true that in theory `db.execute` could ignore semantics and concatenate together the template and variables to make a string without doing any sanitisation, but isn't the same true of the syntax it was claimed to replace?Just because templates (or the previous syntax of passing in variables separately) could be used in a way that's equivalent safety-wise to an f-string by a poorly designed library does not mean that they add nothing over an f-string in general - they move the interpolation into db.execute where it can do its own sanitization and, realistically, sqlite3 and other libraries explicitly updated to take these will use it to do proper sanitization.
imagine writing a SqL where u put user input into query string directly.
now remember its 2025, lie down try not to cry.
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.This way you could encode such identifier directly in the t-string variable rather than with some "out-of-band" logic.
It does, the `Interpolation` object contains an arbitrary `format_spec` string: https://peps.python.org/pep-0750/#the-interpolation-type
However I think using the format spec that way would be dubious and risky, because it makes the sink responsible for whitelisting values, and that means any processing between the source and sink becomes a major risk. It's the same issue as HTML templates providing `raw` output, now you have to know to audit any modification to the upstream values which end there, which is a lot harder to do than when "raw markup" values are reified.
> rather than with some "out-of-band" logic.
It's the opposite, moving it to the format spec is out of band because it's not attached to values, it just says "whatever value is here is safe", which is generally not true.
Unless you use the format spec as a way to signal that a term should use identifier escaping rules rather than value escaping rules (something only the sink knows), and an `Identifier` wrapper remains a way to bypass that.
This should be quiet common in the SQL applications. It will be nice to write t"select {name:id} from {table:id} where age={age}" and be confident that the SQL will be formatted correctly, with interpolations defaulting to (safe) literal values.
print("hello")
def f():
nonlocal foo
gives: SyntaxError: no binding for nonlocal 'foo' found
before printing hello, and note that f() wasn't even called.But wow, that's the first time I've seen "nonlocal". In the ~100 packages I have installed, I see 0 usages!
[1] https://github.com/python/cpython/blob/a6a3dbb7db0516a72c5ef...
`nonlocal` is a keyword
FWIW, format_spec is available in the template structure, so the function writer could at least do a runtime check.
So you would write db.execute(template) to turn template t"... where id = {id}" into a parameterized structure like ("... where id = ?", id).
I completely disagree with this. Look what happened to Log4J when it was given similar freedoms.
As I understand it, log4j allowed malicious ${} expansion in any string passed to logging functions. So logging user generated code at all would be a security hole.
But Python's t-strings purposely _do not_ expand user code, they only expand the string literal.
Also? They tend only to accept a string (possibly with some additional arguments, if there is an in-library way to handle parameterization) as input, because Template literally hasn't been an option. New APIs designed with Template available will look different.
A library can extend an existing database library like 'pg' so that PgClient#query() and PgPool#query() require string template statements.
That way 'pg' can continue working with strings, and people who want nice templated strings can use the small extension library, and the small extension library makes it impossible to accidentally pass strings into the query functions.
Having to write
cr.execute(t"...")
even when there's nothing to format in is not a big imposition.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.
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! :-)
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.
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...
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.
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.
Writing `query: SQL = t"SELECT ..."` is a small price to pay for such a DX boost.
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.
Having more control over the interpolation of string values is a win IMO.
db1 eval {INSERT INTO t1 VALUES(5,$bigstring)}
https://sqlite.org/tclsqlite.html#the_eval_method 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.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.
EF/EF Core has existed for years :)
https://learn.microsoft.com/en-us/ef/core/querying/sql-queri...
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.
This was a completely separate, legacy extension of VS, not EF let alone EF Core.
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?
db.execute(t"Select Count(1) from someTable")
It's one extra letter to "force" for an unparameterized query over a "raw string". The t-string itself works just fine without parameters.There's definitely a backwards compatibility hurdle of switching to a template-only API, but a template-only API doesn't look that much "less friendly" with inputs, when the only difference is a `t` before every string, regardless of number of parameters.
I never put an f in front of a string if I'm not putting variables within it.
And I'm generally used to Python inputs being liberal. I can usually pass a list if it expects a tuple; I can pass an int if it expects a float; often I can pass an item directly instead of a tuple with a single item. Regex functions take regular strings or regex strings, they don't force regex strings.
Being forced to use a single specific type of string in all cases is just very different from how Python has traditionally operated.
It's safer, I get that. But it's definitely less friendly, so I'll be curious to see how module maintainers decide to handle this.
Maybe that's partly the disconnect here? "t-string" is probably a confusing colloquial name because they aren't strings, they are Templates. The runtime type is a Template. It is a very different duck-type from a string. As a duck-typable object it doesn't even implicitly or explicitly act like a string, there's intentionally no `__str__()` method and `str(someTemplate)` doesn't work like you'd expect. It shouldn't be a surprise that there is also no implicit conversion from a string and you have to use its own literal syntax: it isn't a string type, it's a Template type.
Python here is still liberal with respect to Templates (it is still a duck type). If a function expects a Template and you don't want to use the t"" shorthand syntax nor use the Template constructor in string.templatelib, you just need a simple class of object that has an `__iter__()` of the correct shape and/or has `strings` and `values` tuples.
Sure, it may make sense for some types of APIs to support a Union of str and Template as "liberal" options, but it's a different class of liberal support from Union of list and tuple or Union of int and float which are closer "domains" of types. A Template isn't a string and at runtime looks nothing like one (despite how syntactically it looks like one at "compile time"). Given `__iter__()` in Template, it may make more sense/would be more "natural" to Union Template with List or Tuple more than with a single string.
Are you claiming it's traditionally common in Python to be strict with inputs, and that being liberal is the exception?
Yes, it’s common for Python to be strict for inputs when the types are different. For example, try:
Decimal(‘3.0’) / 1.5
You’ll get an error and for good reason.
Decimal('3.0') / 2
It works fine. It doesn't work with a float, which is for good reason. That's the whole point -- its general philosophy is to be pretty liberal with types, except when there's a good reason not to be. Heck, you can even do dumb things like: 4 + True
And get 5 back. If that's not "blindly interchanging different types for no good reason" then I don't know what is. You can even multiply your Decimal object by False and get an answer...Or it's like my original example -- the Regex module isn't restricted to r-strings. It happily works with regular strings. Python's general philosophy is to handle input liberally. Even type hinting is an add-on. Now, it doesn't go as far as JavaScript in allowing e.g. "4"+1, but it's still awfully liberal. I just don't see how you can claim otherwise.
The reason it works is because Python functionally has no bool type. True and False are just integers with names. It’s stupid and shouldn’t work like that but it does for historic reasons.
Your example of regex makes no sense either. There is no difference between strings and r-strings. They’re literally the same thing to the interpreter, so how could the regex functions enforce you use r-strings? Maybe they should be different but, for historic reasons, they can’t be without Python 4.0.
This has not been true since around 2.4 or 2.5. The oldest Python I have available to me currently is 2.7, and this holds then, as it does now in 3.13:
>>> type(True)
<class 'bool'>
>>> type(1)
<class 'int'>
Prior to having a bool type, Python didn't even have True/False keywords.The reason something silly like `4 + True` works is because the bool type implements `tp_as_number` [0]. The reason it works this way is intentional because it would been a Python 3 str-style debacle if ints and bools were not interchangeable.
[0] https://github.com/python/cpython/blob/main/Objects/boolobje...
You seem to be having a different conversation than I am.
I'm just describing Python as it is. I'm not defending it. I know why you can add True to a number, or else I wouldn't have come up with the example. And I know perfectly well that r-strings are just strings. Python easily could have made them a distinct object, to force people from ever making backslash errors, and restricted Regex functions to them, but didn't.
My only point has been, "Pythonic" things tend to be pretty liberal in what they accept. Type hints aren't even enforced, when they exist at all. You seem to think it shouldn't be that way. Great! But regardless, claiming it's not that way -- that Python is somehow this strict language -- is just mischaracterizing it.
Being able to use a string as a string and an int as an int are not “pretty liberal in what they accept,” it’s just programming language theory 101! I think you’re mistaking duck typing for “liberal acceptance,” which are not the same thing. There’s always been an expectation that you should use compatible interfaces, even within the standard library. I’ve been bitten enough times by passing a generator in when a function expects a list, for example.
And there’s a good reason not to be here.
I never put an f in front of a string if I'm not putting variables within it.
Linters will even complain if you have a f string without variables. I assume it will be the same for t strings.You can't; they're different types. t-strings are not `str`
It's up to good framework/API design to take advantage of this.
cursor.execute(“select * from x where foo=?”, {foo=1})
# while also allowing
cursor.execute(t“select * from x where foo={foo}”)
#Vs
cursor.executetemplate(“select * from x where foo={foo}”)
If ‘execute’ takes string and t-string, then I would consider it a problem to use a t-string without parameters. If there is a novel API just for t-strings, then you are implying widespread breaking changes as you have a schism between the two ways of providing parameters.I would argue that as bad as some w3schools tutorials were, and copying from bad Stackoverflow answers, going back to MSA and the free cgi archives of the 90s, the tendency of code snippets to live on forever will only be excarbated by AI-style coding agents.
On the other hand, deprecating existing methods is what languages do to die. And for good reason. I don't think there's an easy answer here. But language is also culture, and shared beliefs about code quality can be a middle route between respecting legacy and building new. If static checking is as easy as a directive such as "use strict" and the idea that checking is good spreads, then consesus can slowly evolve while working code keeps working.
Turns out my editor (vscode) and typechecker (pyright) saw that `datetime.utcnow()` was marked as deprecated (I know one can use the `@deprecated` decorator from Python 3.13 or `__future__` to do this; I think it was done another way in this particular case) and therefore rendered it as struck through.
And it taught me A) that `utcnow()` is deprecated and B) how to mark bits of our internal codebase as deprecated and nudge our developers to use the new, better versions if possible.
I'm thinking in the general case, but motivated by this example of a 3rd party function that accepts a SQL query as a string, and we'd like everywhere in our codebase to stop using that and instead use the 3rd party function that accepts the query as a t-string
def get(self, query):
if isinstance(query, template):
self.get_template(query)
else:
self.get_old(query) #Don't break old code!
Now whether maintainers introduce `getSafe` and keep the old behavior intact, or make a breaking change to turn `get` into `getUnsafe`, we will see
Why do you think changing a letter would cause a vulnerability? Which letter do you mean?
The thing this replaces is every library having their own bespoke API to create a prepared statement on their default/safe path. Now they can just take a template.
Or are you suggesting that e.g. every database module needs to implement a new set of query functions with new names that supports templates? Which is probably the correct thing to do, but boy is it going to be ugly...
So now you'll have to remember never to use 'execute()' but always 'execute_t()' or something.
If a library has functions taking a string and executing it as SQL they probably shouldn’t make that take a template instead, but I’d hope that’s a separate explicitly unsafe function already.
If you want to substitute parameters, you put a '?' in the string for each one, and provide an additional (optional) tuple parameter with the variables.
So no, there's no explicitly unsafe function. That's my point.
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.
It's far more essential than little utilities like textwrap or goliath packages like Python's bundled tkinter implementation.
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.)
Then, you bring up 3rd party templating solutions, but this is off topic, as we are talking about language builtins. I am well aware there are many templating solutions to solve text templating in general.
In summary, I'm specifically asking for examples of languages that allow string interpolation with _deferred/lazy_ processing, which is how I understand this feature. It seems there are a few, but it doesn't seem to be terribly common after reading the other comments.
It's not confusing... it's sarcastic. I don't believe adding literally a third string interpolation method to a language is generally justified, and honestly you can consider it a fourth if you think, as I do, that even "my string " + val + " etc." is itself an interpolation language, especially when combined with things like __str__. I'm afraid you fed me a straight line I couldn't resist; it's not really an answer to you.
Failing that, because it probably would fail, I would be raising the bar sky-high on new feature additions.
It seems like the project has actually lowered the bar in the last few versions. What was once one of my favorite languages, if not my favorite, is now a language I try to avoid, and approaching one I actively recommend against. When I'm writing it myself it's ok, but using other people's code in it is getting harder and harder, as they keep using all the features.
Java 22 had the feature as preview but it was removed in 23, it'll come back after some refinement.
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.
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).
Custom method lookup would just be the same as custom attributes, that is, by the user overriding __getattr__.
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.
;-)
from foo import bar
bar"zoop"
bar(t"zoop")
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.
sql"..."
html"..."
for each of the given examples and achieve some runtime type safety.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.
bar(“zoop”)
It’s just syntax, like we used to have print “foo”
that later became print(“foo”)
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.
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?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.
"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.
(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.)
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.
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}
>>> 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. >>> 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}
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()
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.)
It's definitely true that those four string formatting techniques violate the "one obvious way" advice.
"Situation: There are 14 competing standards...." https://xkcd.com/927/
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.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
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__.
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.
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.
Meanwhile, pytest is still not part of the standard library.
https://www.psycopg.org/psycopg3/docs/api/sql.html
(while I also agree it gets crowded with yet another string prefix)
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.
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>"
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.
Paging asottile - any plans to make a `future-tstrings`? :)
`future-fstrings` (https://pypi.org/project/future-fstrings/) was a real QOL improvement for our team for a year or 2 around 2019 before we got onto Python 3.x.
In fact, the repo of a companion project from the author has the ticket that spawned the work leading to t-strings: https://github.com/jviide/htm.py/issues/11
As for variables and arbitrary code/lambdas, yes: t-strings can do that, just like f-strings
The syntax is template literals, not just "tagged templates". Which is a huge difference: template literals still act as real strings. They don't need a tag prefix to work, you have the option to tag them if and when needed.
As far as I understand it, t-strings can't do that. They're not strings, and you can't even coerce them into strings, you have to run them through a processor before they become a string. So they're nothing like JS's template literals, they're syntactic sugar for forming "an instance of an object that needs to be passed into a function that returns a string".
So I don't look forward to folks preferring f-strings over t-strings even when they really shouldn't, simply because "having to constantly convert them from not-a-string to a string is a hassle". If only they'd worked like JS template literals.. that would have been fantastic.
> To support processing, `Template`s give developers access to the string and its interpolated values before* they are combined into a final string.*
Are there any use-cases where processing a Template involves something other than (i) process each value, then (ii) recombine the results and the string parts, in their original order, to produce a new string? In other words, is the `process_template` function ever going to be substantially different from this (based on `pig_latin` from the article)?
def process_template(template: Template) -> str:
result = []
for item in template:
if isinstance(item, str):
result.append(item)
else:
result.append(process_value(item.value))
return "".join(result)
I haven't seen any examples where the function would be different. But if there aren't any, it's strange that the design requires every Template processing function to include this boilerplate, instead of making, say, a `Template.process` method that accepts a `process_value` function.Also, in addition to the other SQL example using "?" to fill in the "holes" for parameters in an SQL friendly way, some DBs also support named parameters, so the "hole" in the string form might be naively replaced with something like `f"@{item.expression}"` and that also forms the key in a dict to pass as parameters. (You'd want to make sure that the expression inside the template is useful as a parameter name, and not something more exotic like {1 + 3} or {thing for thing in some_list}, in which cases you are probably auto-assigning some other parameter name.)
def process_template(template: Template) -> tuple[str, tuple]:
sql_parts = []
args = []
for item in template:
if isinstance(item, str):
sql_parts.append(item)
else:
sql_parts.append("?")
args.append(process_value(item.value))
return "".join(sql_parts), tuple(args)
(of course it would be more nuanced, but I hope you get the point)Also, my comment was about the amount of boilerplate required, but that can be vastly reduced by writing `process_template` in a more functional style instead of the highly-imperative (Golang-like?) style used in the article. The first `process_template` example is just:
def process_template(template: Template) -> str:
return ''.join(interleave_longest(template.strings, map(process_value, template.values)))
And the second is something like: def process_template(template: Template) -> tuple[str, tuple]:
return (
''.join(interleave_longest(template.strings, ['?'] * len(template.values))),
map(process_value, template.values)
)
Other replies gave examples of other use cases. But the neat thing about Python is that you don't need to "include this boilerplate" for the common cases. It can be wrapped up in a decorator (which could be included in `templatelib`). Or, as you say, in a method on the Template class.
I think I'd implement it as a generator, calling `process_value` (defaulting to `str`) on the Interpolations, so that the caller can still do more with the results (or just `''.join` them).
But these are separate considerations; nothing prevents implementing them later, or indeed adding them to the implementation before the 3.14 release.
html`<p>${value}</p>` will actually run the function html(template). This means you can use this to "mark" a function in a way that can be detected by static analysis. Many editors will, for example, syntax highlight and lint any HTML marked this way, same with SQL, GraphQL and probably some others too.
html_string = t"<something />"
sql_string = t"SELECT * FROM something"
In JS, the string has a prefix that can differ between languages, e.g.: const htmlString = html`<something />`
const sqlString = sql`SELECT * FROM something`
and so on. See the difference?Please engage with my point instead of criticizing trivialities.
Your complete misunderstanding of what's happening is not a triviality.
> The essential difference is that the language is referenced at the declaration site, not the usage site, which makes the syntax highlighting far easier.
Javascript has no built-in template tags beyond `String.raw`. If tooling has the capabilities to infer embedded language from arbitrary third party libraries, I would hope they have the ability to do utterly trivial flow analysis and realise that
html(t"<something />")
means the template string is pretty likely to be HTML content.IN other words, since custom template tags in JS *are literally just function calls* when a JS environment syntax highlights the code as HTML it's doing so based on an extremely weak heuristic (the identifier for the interpolation function is named "html"). Both Python and JS have the same problem.
And it's obviously more complex to do syntax highlighting when the declaration site and usage site are possibly split apart by variable assignments etc. Yes, in the case you showed syntax highlighting is easy, but what if the `html` function takes more parameters, doesn't take the template as the first parameter, etc? There's a lot of possible complexity that tagged template literals don't have. Thus they are easier to do highlighting for. This is objectively true.
You can't split apart the declaration of the template literal and the "tagging". The tag is always part of the declaration, which it doesn't have to be in Python, as I've showed.
You can't pass additional parameters to the tag function, it's always just the template & values. In Python, you can pass as many parameters as you want to the usage site, e.g.
some_value = html(True, t"<something />", 42)
In Python you can do this:
bar = "hello world"
template = t"<something foo={bar} />"
string = html(template)
This is simply not possible in JS, because the template literal always must have the tag attached. You can't split them apart. If you try: const bar = "hello world"
const template = `<something foo=${bar} />`
you already have a string in the `template` variable. There's no access to the individual values anymore. It's already done. It's a string. No template. You can't pull apart the literal declaration and the tagging.Are we now done with this ridiculous game of deliberate misunderstandings?
Python should be able to detect the magic syntactic pattern just the same way JS does and use that for syntax higlighting. In JS the magic syntactic pattern for triggering HTML syntax highlighting is:
html`<doc/>`
In Python the magic syntactic pattern would be: html(t"<doc />")
My point in showing that JS counterexample was to demonstrate that the real reason people don't do that kind of thing isn't that they can't, it's that they like having syntax highlighting. That means the approach should work just as well in Python. This is the case even though the heuristic involved is very weak. Changing the name of the identifier or breaking into multiple expressions would be enough to break the heuristic in either language, which is why I think it's a really weak heuristic and dangerous pitfall for developers who might mistake the coloring for information about how the runtime sees that data (which is normally what syntax highlighting is for)Had you not played games with this technicality, we could both have saved a lot of time. Hope you had fun I guess.
let passthrough = (...args) => args;
let bar = "hello world";
let template = passthrough`<something foo=${bar} />`;
string = html(...template);
Now, I'll just ignore that you're still deliberately misrepresenting me in the hopes of gaining some actual knowledge from this discussion - please show me: which editor supports syntax highlighting for the specific example you just mentioned? After all, that was the topic of discussion - not whether it's possible to delay "tagging", but whether it's easier to do syntax highlighting for JS template tags. Please show me an existing editor, IDE or git repo for a tool that would highlight your specific example based on the `html(...template)` call in line 4 (so no heuristics based on line 3). Surely you're not just throwing random examples at the wall to see what sticks, right? You've actually been following the discussion and are arguing in the context of it?
You can do that in python by accessing the `strings` and `values`, but I expect most cases will simply iterate the template, yielding a unified typed view of literal strings and interpolated values.
I actually quite like the simplicity of this design over tagged literals in JS.
In other words, t-strings are basically f-strings where the final concatenation is delayed. And indeed, you can trivially implement f-strings using t-strings by performing a simple, non-escaped concatenation step: https://peps.python.org/pep-0750/#example-implementing-f-str...
f'...' -> str
t'...' -> Template
foo(t: Template) -> str
This sounds like unnecessary fluff in what was supposed to be a simple language. I'm worried Python is turning into C++42 with 65535 ways to do one simple thing.
Why not just:
f'SELECT * FROM `{esc(table)}` WHERE name = "{esc(name)}"'
Nice and simple.Most DBs support parameterized queries which can be cached for performance. How do you pick out the parameters from that and replace those parts of the strings with the DB's parameter placeholders?
t'Select * from {table} where name = {name}'
Looks very similar, but execution engine has access to all the individual parts, making it very easy to add placeholders such as: ('Select * from ? where name = ?`, table, name)
Or even (if the DB supports it), has access to the expressions inside the string and can use named parameters: ('Select * from @table where name = @name', { "table": table, "name": name })
That's really nice for debugging, depending on your DB engine.In every DB engine that supports it, parameterized SQL is even safer than escape syntaxes because parameters are passed in entirely different parts of the binary protocols and don't need to rely on just string manipulation to add escape sequences.
The "subsequent logic" has full access to the interpolation results and strings. Not only can it escape the results, it can do whatever it wants to them. It can also do whatever it wants to the strings, and then combine everything in any way it likes - it's not even necessary that the final result is a string.
name = "World"
template = t"Hello {(lambda: name)}"
This looks cool
I supposed when assigning it to a, variable: SyntaxRecognizableTemplate, you could give it the hint necessary.
was this discussed in the PEP?
*edit: reading the PEP-750[1] it doesn't seem like it..
[1] https://peps.python.org/pep-0750/#the-interpolation-type
But yes, the PEP leaves open an important question: how will tools decide to work with common types of content in t-strings, like HTML or SQL?
There are simple approaches that can be taken in the short term (content sniffing) and more robust approaches (type annotations, perhaps) that will take time and the broader tooling community to develop.
As an example, I was excited about using `Annotated` on the function to indicate the language it expected. Turns out, a lot of linters know nothing about the type system.
name="A$ron"
z("echo Hello {name}")
Note that this is not an f-string. The z function expands the variables by parsing this string and accessing its caller's local variables.
from sql import sql
query = sql"SELECT user_id, user_name FROM {user_table_versioned} WHERE user_name = {user_name}"
The syntactic sugar of changing it from sql(t"...") doesn't seem particularly valuable. The novel thing about t-strings is that they change the parsing at compile-time.
It's valuable because:
- IDEs could then syntax-highlight SQL inside of SQL strings and HTML inside of HTML strings
- You can't accidentally pass an HTML string to your SQL library
There’s nothing stopping you from building a Python function that parses a string looking for {} and then searching globals for those variables. And you can extend that to also do some code execution and formatting.
To me the real sugar of f-strings is that the editor knows that it’s a template and not just a string. Expanding this to having SQL and regex syntax highlighting, linting and code formatting inside my Python code is a pretty cool prospect.
Your sql there would just be a function that receives the array of strings/values and returns whatever.
The concept of prefixes itself feels a little deviant from readable code that is close to human language -- which is the spirit of Python
a = template "foo {bar}"
As should raw and format.Also, don’t get me started on g strings.
No
> They are just a special case of t strings.
Not really, because they produce a string right away instead of a template.
cur.executemany("INSERT INTO movie VALUES(?, ?, ?)", data)
Can SQLite3 cache the query as it does now?The template object itself could not be formed because the each name must be a visible variable name.
This is the same error that you would get with an f-string that contained an undefined variable name.
cur.executemany(t"INSERT INTO movie VALUES({data})")
Then the Template object would have a parameter that is a list, and it could turn that into the right number of "?"s to pass into the database driver along with the data.What am I missing?
The only reason I could imagine, is if you are trying to protect developers from themselves, which kinda goes against the "we're all adults here" mentality that makes Python so great. I suppose it's easy enough to add that functionality, but come on.
I honestly feel like a lot of people just seem bored and looking for stuff to be mad about.
f"foo is {foo} and {bar=}"
"foo is {} and bar={}".format(foo, bar)
are equivalent.t-strings are actually not strings, but Template objects, giving access to both the templating string and the parameters for processing. Sibling comments describe it as a custom .format implementation in that sense - it's f-string-like sugar where you're also allowed to take control of the .format function that it's sugar for.
t-strings, of course, (will) translate at the bytecode level into instantiation of the Template object (and whatever code is needed to compute the Interpolation values in-place).
log.debug(f"The value of counter was {counter}, the nonce was {nonce}")
builds a new string every time the interpreter hits this line. Whereas log.debug(t"The value of counter was {counter}, the nonce was {nonce}")
passes a Template to the debug() function that bails out if debug mode is not on and doesn't build a string.Seems like a self selection which renders this meaningless, to some extent :/
t-strings are a different type (both static and dynamic), f-strings are not. So t-strings can be mandated at the API level, forcing the developer into "proper" usage.
That is, you need third-party tools to differentiate between
some_call("safe literal string")
and some_call(f"unsafe dynamically created string")
That is not the case when it comes to t-strings, `some_call` can typecheck internally that it got a t-string, and reject regular strings entirely.Although some space probably needs to be made for composing t-strings together in case of e.g. runtime variation in items or their count. Facetting for instance. I don't know if that's a native affordance of t-strings.
This is of the category "things I wouldn't want to use even for the specific hyper niche things they're intended for". What even does a "t-string" represent? Because it's clearly not a string of any kind, it's a weird kind of function call notation. The programmer sees something that looks like string formatting, but the program executes some arbitrary procedure that might not return a string whatsoever.
I don't think this is the right idiom for doing this. Frankly I don't think SQL query generation should look like string templating at all!
The sell seems to be "now you can write code that looks like an SQL injection vulnerability, without it actually being vulnerable!". I'd rather write code that isn't a vulnerability, and doesn't look like one, and doesn't have to bend the language grammar either.
It's like even the one case identified nobody has even thought all the way through. Now your SQL library only accepts t-strings, I get an obscure error passing in a simple static query. Ah yes, put the useless t on it. That sorted, now the SQL library escapes all the parameters it wasn't previously doing, to then hand the final unique query to the actual underlying SQL library which would much rather have the parameterized one so it can cache parsing. Jesus.
Python has historically been very conservative about this but in recent years has had one controversial language extension after another, while parts of the language that actually need love are left to languish IMO.
I wanna be very clear that this is me changing my mind -- I was (still am) very on board with the highly controversial assignment expressions ("walrus operator") for instance.
I don't have much faith about what the Python language will look like if you project the current rate of changes forward 10, 15, 20 years. It really doesn't help that I consider this new thing an active antifeature.
And I love Python but, having been through 2->3 ( occasionally still going through it! ) whenever I see a new language feature my first thought is "Thank goodness it doesn't break everything that went before it".
I've been programming with Python since 2006, I think most of the systems were based on 2.4 at the time. I've been one of those who switched to Python 3 somewhat late, waiting for some major libraries to ship python 3 packages - celery and Twisted were one of the biggest holdouts - so I remember that the first project where all my dependencies were ready for python 3 was around 2015.
This is to say: even seasoned developers who were conservative around the migration have spent more time working with Python 3 than Python 2. There simply is no reason anymore to be talking about python 2.
Buuuttt, I'm so over the transition. It’s ancient now and I agree that we can stop fretting about it.
We are not completely Post Traumatic Python2 Stress yet, I am afraid.
Bad decisions can have looong-term repercussions.