I would like to have a mechanism which evaluates an f-string where the contents to be evaluated are provided inside a variable. For example,
x=7
s='{x+x}'
fstr_eval(s)
For the usage case I have in mind, the string s
may arise from user input (where the user is trusted with eval
).
While using eval
in production is generally very bad practice, there are notable exceptions. For instance, the user may be a Python developer, working on a local machine, who would like to use full Python syntax to develop SQL queries.
Note on duplication: There are similar questions here and here. The first question was asked in the limited context of templates. The second question, although very similar to this one, has been marked as a duplicate. Because the context of this question is significantly different from the first, I decided to ask this third question based on the automatically-generated advice following the second question:
This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.
Even with a trusted user, using eval
should only be a very last resort.
If you are willing to sacrifice flexibility of your syntax for a bit more security and control, then you could use str.format
and provide it your whole scope.
This will disallow evaluation of expressions, but single variables will be formated into the output.
x = 3
y = 'foo'
s = input('> ')
print(s.format(**vars()))
> {x} and {y}
3 and foo
Here is my attempt at more robust evaluation of f-strings, inspired by kadee's elegant answer to a similar question.
I would however like to avoid some basic pitfalls of the eval
approach. For instance, eval(f"f'{template}'")
fails whenever the template contains an apostrophe, e.g. the string's evaluation
becomes f'the string's evaluation'
which evaluates with a syntax error. The first improvement is to use triple-apostrophes:
eval(f"f'''{template}'''")
Now it is (mostly) safe to use apostrophes in the template, as long as they are not triple-apostrophes. (Triple-quotes are however fine.) A notable exception is an apostrophe at the end of the string: whatcha doin'
becomes f'''whatcha doin''''
which evaluates with a syntax error at the fourth consecutive apostrophe. The following code avoids this particular issue by stripping apostrophes at the end of the string and putting them back after evaluation.
import builtins
def fstr_eval(_s: str, raw_string=False, eval=builtins.eval):
r"""str: Evaluate a string as an f-string literal.
Args:
_s (str): The string to evaluate.
raw_string (bool, optional): Evaluate as a raw literal
(don't escape \). Defaults to False.
eval (callable, optional): Evaluation function. Defaults
to Python's builtin eval.
Raises:
ValueError: Triple-apostrophes ''' are forbidden.
"""
# Prefix all local variables with _ to reduce collisions in case
# eval is called in the local namespace.
_TA = "'''" # triple-apostrophes constant, for readability
if _TA in _s:
raise ValueError("Triple-apostrophes ''' are forbidden. " + \
'Consider using """ instead.')
# Strip apostrophes from the end of _s and store them in _ra.
# There are at most two since triple-apostrophes are forbidden.
if _s.endswith("''"):
_ra = "''"
_s = _s[:-2]
elif _s.endswith("'"):
_ra = "'"
_s = _s[:-1]
else:
_ra = ""
# Now the last character of s (if it exists) is guaranteed
# not to be an apostrophe.
_prefix = 'rf' if raw_string else 'f'
return eval(_prefix + _TA + _s + _TA) + _ra
Without specifying an evaluation function, this function's local variables are accessible, so
print(fstr_eval(r"raw_string: {raw_string}\neval: {eval}\n_s: {_s}"))
prints
raw_string: False
eval: <built-in function eval>
_s: raw_string: {raw_string}\neval: {eval}\n_s: {_s}
While the prefix _
reduces the likelihood of unintentional collisions, the issue can be avoided by passing an appropriate evaluation function. For instance, one could pass the current global namespace by means of lambda
:
fstr_eval('{_s}', eval=lambda expr: eval(expr))#NameError: name '_s' is not defined
or more generally by passing suitable globals
and locals
arguments to eval
, for instance
fstr_eval('{x+x}', eval=lambda expr: eval(expr, {}, {'x': 7})) # 14
I have also included a mechanism to select whether or not \
should be treated as an escape character via the "raw string literal" mechanism. For example,
print(fstr_eval(r'x\ny'))
yields
x
y
while
print(fstr_eval(r'x\ny', raw_string=True))
yields
x\ny
There are likely other pitfalls which I have not noticed, but for many purposes I think this will suffice.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With