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.
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.https://www.psycopg.org/psycopg3/docs/api/sql.html
(while I also agree it gets crowded with yet another string prefix)
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.
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.
from foo import bar
bar"zoop"
bar(t"zoop")
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”)
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.
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.
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.
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 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.
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
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.
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.Having more control over the interpolation of string values is a win IMO.
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.
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.Meanwhile, pytest is still not part of the standard library.
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.
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.
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__.
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.
name = "World"
template = t"Hello {(lambda: name)}"
This looks cool
As for variables and arbitrary code/lambdas, yes: t-strings can do that, just like f-strings
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.
Having to write
cr.execute(t"...")
even when there's nothing to format in is not a big imposition.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).
At least db.safe says what it does, unlike t".
`db.execute(Template)` and `db.unsafeExecute(str)`
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.
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.
db.execute("SELECT * FROM table WHERE id = ?", (row_id,))
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.
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.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).
imagine writing a SqL where u put user input into query string directly.
now remember its 2025, lie down try not to cry.
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.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.
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] )"`.
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.
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.
Agree. And the mere presence of such a feature will trigger endless foot-gunning across the Python database ecosystem.
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.
Python is notorious for misguided motivations. We're not "appealing to authority" here. We're free to point out when things are goofy.
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.> 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.
> Definitely true.
The rest of your comment is valuable, but this is just mean-spirited and unnecessary.
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.
> 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.
I completely disagree with this. Look what happened to Log4J when it was given similar freedoms.
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.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 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.
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.
> 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. 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)
)
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.)
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.
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.
It's far more essential than little utilities like textwrap or goliath packages like Python's bundled tkinter implementation.
Java 22 had the feature as preview but it was removed in 23, it'll come back after some refinement.
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 actually quite like the simplicity of this design over tagged literals in JS.
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.
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.
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.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.
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.
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? cur.executemany("INSERT INTO movie VALUES(?, ?, ?)", data)
Can SQLite3 cache the query as it does now?
gnabgib•11h ago
damnitbuilds•6h ago
;-)