How to postpone/defer the evaluation of f-strings?
Here's a complete "Ideal 2".
It's not an f-string—it doesn't even use f-strings—but it does as requested. Syntax exactly as specified. No security headaches since we are not using eval()
.
It uses a little class and implements __str__
which is automatically called by print. To escape the limited scope of the class we use the inspect
module to hop one frame up and see the variables the caller has access to.
import inspect
class magic_fstring_function:
def __init__(self, payload):
self.payload = payload
def __str__(self):
vars = inspect.currentframe().f_back.f_globals.copy()
vars.update(inspect.currentframe().f_back.f_locals)
return self.payload.format(**vars)
template = "The current name is {name}"
template_a = magic_fstring_function(template)
# use it inside a function to demonstrate it gets the scoping right
def new_scope():
names = ["foo", "bar"]
for name in names:
print(template_a)
new_scope()
# The current name is foo
# The current name is bar
Using .format is not a correct answer to this question. Python f-strings are very different from str.format() templates ... they can contain code or other expensive operations - hence the need for deferral.
Here's an example of a deferred logger. This uses the normal preamble of logging.getLogger, but then adds new functions that interpret the f-string only if the log level is correct.
log = logging.getLogger(__name__)
def __deferred_flog(log, fstr, level, *args):
if log.isEnabledFor(level):
import inspect
frame = inspect.currentframe().f_back.f_back
try:
fstr = 'f"' + fstr + '"'
log.log(level, eval(fstr, frame.f_globals, frame.f_locals))
finally:
del frame
log.fdebug = lambda fstr, *args: __deferred_flog(log, fstr, logging.DEBUG, *args)
log.finfo = lambda fstr, *args: __deferred_flog(log, fstr, logging.INFO, *args)
This has the advantage of being able to do things like: log.fdebug("{obj.dump()}")
.... without dumping the object unless debugging is enabled.
IMHO: This should have been the default operation of f-strings, however now it's too late. F-string evaluation can have massive and unintended side-effects, and having that happen in a deferred manner will change program execution.
In order to make f-strings properly deferred, python would need some way of explicitly switching behavior. Maybe use the letter 'g'? ;)
It has been pointed out that deferred logging shouldn't crash if there's a bug in the string converter. The above solution can do this as well, change the finally:
to except:
, and stick a log.exception
in there.
A concise way to have a string evaluated as an f-string (with its full capabilities) is using following function:
def fstr(template):
return eval(f"f'{template}'")
Then you can do:
template_a = "The current name is {name}"
names = ["foo", "bar"]
for name in names:
print(fstr(template_a))
# The current name is foo
# The current name is bar
And, in contrast to many other proposed solutions, you can also do:
template_b = "The current name is {name.upper() * 2}"
for name in names:
print(fstr(template_b))
# The current name is FOOFOO
# The current name is BARBAR
This means the template is a static string with formatting tags in it
Yes, that's exactly why we have literals with replacement fields and .format
, so we can replace the fields whenever we like by calling format
on it.
Something would have to happen to the string to tell the interpreter to interpret the string as a new f-string
That's the prefix f/F
. You could wrap it in a function and postpone the evaluation during call time but of course that incurs extra overhead:
template_a = lambda: f"The current name is {name}"
names = ["foo", "bar"]
for name in names:
print (template_a())
Which prints out:
The current name is foo
The current name is bar
but feels wrong and is limited by the fact that you can only peek at the global namespace in your replacements. Trying to use it in a situation which requires local names will fail miserably unless passed to the string as arguments (which totally beats the point).
Is there any way to bring in a string and have it interpreted as an f-string to avoid using the
.format(**locals())
call?
Other than a function (limitations included), nope, so might as well stick with .format
.