Template strings variant where expressions are not evaluated (original) (raw)
The upcoming template strings (PEP-750) would almost suit me well for one particular use-case of mine. However these t-strings process exceptions the same way as f-strings. I’d like to start a discussion if there are enough other use-cases where this is undesired.
I’m having log lines like this:
log(f"frequency = {1/period}; temp = {convert(temp)}")
and I want to suppress exceptions occuring during evaluation of possibly invalid data as much as possible. For example if period
is zero or None
, I still want to log the temperature. Currently I need one try-except
block for each expression (+4 lines per logged value whenever evaluation could fail).
I haven’t tried (no access to Python 3.14), but I think the best I will be able to do (with eval
and locals from caller’s stack frame) is:
log(t"frequency = {'1/period'}; temp = {'convert(temp)'}")
# note the expressions are now strings
What I would like to have is a template string variant that:
- either does not evaluate anything, it just stores the expression as-is,
- or it saves either a value (result) or the exception (error) occurred during evaluation.
JamesParrott (James Parrott) May 5, 2025, 8:46am 2
I think you should write a custom logger, using the %s
string syntax. And Python should not introduce such a potentially problematic and even dangerous feature.
xitop (Xitop) May 5, 2025, 1:33pm 3
Could you please be more specific about what you dislike?
brass75 (Dan Shernicoff) May 5, 2025, 1:54pm 4
In most cases the exceptions are actually desirable. For your use case you should use a function call with a try/except
to handle them. Quietly ignoring exceptions at the language level is, at best problematic and more often extremely dangerous.
xitop (Xitop) May 5, 2025, 2:33pm 5
I was not proposing to quietly ignore exceptions. I was proposing storing them for later processing. Just like asyncio.gather
with the return_exceptions=True
parameter. And it should be the main feature of this new tool, not an obscure detail. The programmer could use it if he or she finds it useful for some minor task.
In my case the difference is this: I can have many repeated try-except blocks spreaded throughout the whole program OR I can have a single log function that will do the same but in one place.
JamesParrott (James Parrott) May 5, 2025, 4:59pm 6
- You can just log
period
instead of1/period
, and add a little helper function to safely calculate1/period
. - It’s a huge task to define the interaction of these new strings, with existing exception mechanics, e.g.
raise Exception(T"{raise Exception}"
- You’re now saying after using a feature implemented with specific syntax, the programmer must remember to call something like
asyncio.gather
. - The spirit of “Errors should never pass silently, unless explicitly silenced.” does not mean a new single character should be added to the language’s syntax to do the silencing.
- Error suppression with
try/except
is a huge source of gotchas and footguns in Python. It should be expensive (in terms of LOC), and coders should be forced to treat it properly, and give it thought. It is one of the very last features of the language that should be made easier with syntactic sugar.
I could probably go on, but I’ll stop there.
xitop (Xitop) May 5, 2025, 7:22pm 7
I don’t understand some of your points in the context of my post. I do not recognize some parts of it as coming from my proposal.
All focus in replies received so far went to the alternative part of my proposal. However I wrote either do not evaluate (no exceptions at all this way) - or save the result/error without raising. (I mentioned asyncio.gather
as an existing example for the latter approach.)
I will repeat the proposal (the first alternative only) on an example:
Let’s call the new feature “N-string” as a working name. This line:
N"this is an {example} of a template {tid}"
would return an object structured exactly like the Template
as defined in PEP-750. This template would reference two objects very similar to the Interpolation
type from PEP-750. The only difference is that interpolation.value
attribute would be unused (not present or not filled in). The caller is now responsible to determine what values example
and tid
represent in this template. This is how it differs from t-strings.
This would be helpful for my use-case (which is really not important here) and maybe to some other use-cases as well.
JamesParrott (James Parrott) May 6, 2025, 8:05am 8
If suppressing exceptions isn’t required, and is just a side effect of deferred evaluation, then that’s great. I can see the appeal of lazily evaluating the sub fields of an f-string or t-string on demand.
If they’re going to be stored as strings of the expression text anyway, why not use a normal string? Surely there’s a straightforward way of parsing the fields in balanced pairs of unescaped undoubled braces, e.g a regex full match?
xitop (Xitop) May 6, 2025, 1:02pm 9
No problem with an own custom parser. Long time ago I was once teaching an introductory course.
My point is that 99% of the required work will be present in the Python soon. I was hoping I’m not the only one who would welcome having a built-in reliable well-documented template string parser allowing to give the {placeholders} application specific meaning (within limits of the t-string grammar).
The missing 1% could be done - as a minimum required - by exposing a function parsing t-strings that stops just before the (last?) step called “Interpolation Expression Evaluation” (a term from PEP-750). There are of course alternatives and details to discuss.
xitop (Xitop) May 6, 2025, 1:07pm 10
Note: I changed the title.
The alternative with exceptions saved and not propagated got a negative feedback. Please let’s continue the discussion with template strings where {interpolation expressions}
are not evaluated at all.
Rosuav (Chris Angelico) May 6, 2025, 1:09pm 11
If they’re not being evaluated, is there any benefit to having a template string at all? All you’re getting is the text of it, so you can do all of the parsing yourself with no loss of functionality.
JamesParrott (James Parrott) May 6, 2025, 2:56pm 12
The fields would be subject to syntax parsing and code quality checks by the standard code tools.
More importantly, as they would evaluate fields lazily, similarly to generator expressions, I propose the new string type be prefixed by a g
. So for example they’d look like: g"Possible error value: {1/0}"
.
Then Python would have r-strings, f-strings, t-strings, and …
Rosuav (Chris Angelico) May 6, 2025, 3:12pm 13
That can be done with simple string literals too. Lots of tools can validate regular expressions, SQL queries, and other such minilanguages, despite them being string literals with no language-level adornment.
xitop (Xitop) May 6, 2025, 3:19pm 14
Basically you are right, but:
- the t-string parser already does everything needed, it is (sorry, it will be soon) built-in and documented. Also it is not as simple as it might look or as an ad-hoc parser will probably be, e.g. I’d expect that
t"Value: {value:.{precision}f}"
will set the.format_spec
attribute properly even if the.value
will be skipped. - I’m trying to figure out if there is any demand for this feature. At least it is not among “rejected ideas” in the PEP.
pf_moore (Paul Moore) May 6, 2025, 3:38pm 15
It sounds to me like it’s something that would require a PEP of its own. So the “easy” way to find out if there’s demand is to write a pre-PEP, and put it up for discussion.
My personal feeling is that there probably isn’t a demand for the feature. But these days, I feel as though people tend to want to continue pushing for their ideas, even in the face of lukewarm, or even mildly negative, feedback. So if you want to continue anyway, I suggest you write up your idea in PEP form, for Python 3.15. I’ll likely be -1 on the proposal, maybe you’ll get some supporters, though.
oscarbenjamin (Oscar Benjamin) May 6, 2025, 3:57pm 16
Early PEP 750 discussion debated whether it should be possible for the expressions to remain unevaluated somehow. That idea was rejected with the conclusion being that lambda could be used for unevaluated expressions if needed:
log(t"frequency = {lambda: 1/period}; temp = {lambda: convert(temp)}")
Now your log function can use try/except
when calling the functions.
xitop (Xitop) May 6, 2025, 4:48pm 17
As there is now no reason to continue this thread, I’d like to thank everybody who took part in this discussion and shared their insights.
gerardw (Gerardwx) May 6, 2025, 4:59pm 18
Are t-strings supposed to work that way? I’m being told I needed parentheses:
>>> period = 2
>>> t'frequeny = {lambda: 1/period}'
File "<python-input-1>", line 1
t'frequeny = {lambda: 1/period}'
^^^^^^^
SyntaxError: t-string: lambda expressions are not allowed without parentheses
effigies (Chris Markiewicz) May 6, 2025, 5:27pm 19
I suspect that restriction is intended to avoid reader ambiguity (the error shows there is no parser ambiguity) for t'{lambda:format_spec}'
, and the PEP includes parens:
If a single interpolation is expensive to evaluate, it can be explicitly wrapped in a
lambda
in the template string literal:name = "World" template = t"Hello {(lambda: name)}" assert callable(template.interpolations[0].value) assert template.interpolations[0].value() == "World"
Just to follow-up on the original goal, it seems like you could write a safe()
function to wrap either individual lambdas or lambda-containing templates:
def safe(t: Template) -> Template: ...
log(safe(t"frequency = {(lambda: 1/period)}; temp = {convert(temp)}"))
def safe(f: Callable[[], R]) -> R | None:
with suppress(Exception):
return f()
log(t"frequency = {safe(lambda: 1/period)}; temp = {convert(temp)}")
I kind of like the idea of composable template preprocessors, though the latter seems simpler.
JamesParrott (James Parrott) May 6, 2025, 10:18pm 20
I’m more than happy to write a draft PEP. As long as we can all agree the prefix for these new literals should be a g
.