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.
effify('asdf"')
produces a syntax error. There's a reason my solution is so complicated. :) – Lauds