Thank you all for the kind and useful feedback on my suggestion!
I definitely agree now that introducing a new keyword for this use case is just too breaking for such a relatively niche feature is not viable and should not happen!
I like the approach from @CarrotManMatt and it is an interesting idea, similar to how types were first introduced to the language (via comments). This comment syntax could be highlighted by IDEs and supported for renaming variables and similar features.
I also like the ideas and brainstorming @latk brought to the table, but like they said themselves, I also don’t quite like the \E
way of escaping expressions.
New ideas
So I went back to the drawing board and thought about it some more and have
three things I could think of:
The best solution for Python 3.14
I found a very nice way of using Python 3.14’s templates for our purpose.
These have many useful features, but the one that interests us is the expression
field of interpolations. We can use the soon supported generic support of templates and interpolations, combined with the previously mentioned expression field to create this:
Code sample in pyright playground
from string.templatelib import Template
from typing import Literal, overload, reveal_type
@overload
def expr[T](template: Template[T], eval: Literal[True], /) -> tuple[str, T]:
...
@overload
def expr[T](template: Template[T], eval: Literal[False], /) -> str:
...
@overload
def expr[T](template: Template[T], /) -> str:
...
def expr[T](template: Template[T], eval: bool = False, /) -> str | tuple[str, T]:
if len(template.interpolations) != 1:
raise ValueError("expected exactly 1 interpolation")
if template.strings[0] or template.strings[1]:
raise ValueError("strings must be empty")
interp = template.interpolations[0]
if interp.format_spec or interp.conversion:
raise ValueError("no format spec allowed")
if eval:
return (interp.expression, interp.value)
return interp.expression
# Python 3.14rc1 does not yet have generics for Template and Interpolation yet,
# which is why it doesn't work YET
reveal_type(expr(t'{10}'))
reveal_type(expr(t'{10}', False))
reveal_type(expr(t'{10}', True))
So yes, we can use very concise syntax to create dynamic expressions. There are two caveats though:
- Because templates are evaluated eagerly, the expression will always be evaluated and can therefore cause major runtime costs if only the string is needed.
- Minor, but the syntax could still be shorter if possible.
If templates were actually lazy, then I would say this implementation is definitely sufficient and no new feature is needed (except maybe making it a builtin function). It is even a rejected idea of PEP 750, but another PEP in the future might bring something similar to the table.
So, we either hope that happens, otherwise I have another idea.
e-strings
e-strings are very similar to to all the concepts we previously discussed. They would look something like this:
expr: str = e'stars[0]'
result: tuple[str, T] = e'stars[0]=' # T is the type of the evaluated expression, here int
But honestly, this is not a good solution for multiple reasons. I just wanted to mention it, because I liked the idea at the start.
A typing approach
I suggest we could add a new function called expr
to either the typing, inspect or builtin module. It would have the following signature:
def expr(e: str, eval: bool = False, /, globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None) -> str | tuple[str, Any]: ...
- It has similar overloads as mentioned in my templates approach above, so if eval is
True
, a tuple with the string and value is returned, otherwise just the string (but lazy!). - This function is special cased by type checkers, so instead of
Any
the type checker and IDE understand that the passede
string is supposed to represent be a valid expression and possibly highlight it accordingly. If the expression seems invalid, this should also be highlighted of course. Renaming and other features should possibly also be supported for this string. - The code of this function is simply this:
def expr(e: str, eval: bool = False, /, globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None) -> str | tuple[str, Any]:
if eval:
return (e, eval(e, globals, locals))
return e
I would totally love this approach, if globals and locals didn’t have to be explicitly passed if needed, but oh well.
I also just need to mention how smoothly this would fit next to the functions eval
and exec
in terms of naming and functionality.
So let me know what you think of my 3 ideas!