218

I am using template strings to generate some files and I love the conciseness of the new f-strings for this purpose, for reducing my previous template code from something like this:

template_a = "The current name is {name}"
names = ["foo", "bar"]
for name in names:
    print (template_a.format(**locals()))

Now I can do this, directly replacing variables:

names = ["foo", "bar"]
for name in names:
    print (f"The current name is {name}")

However, sometimes it makes sense to have the template defined elsewhere — higher up in the code, or imported from a file or something. This means the template is a static string with formatting tags in it. Something would have to happen to the string to tell the interpreter to interpret the string as a new f-string, but I don't know if there is such a thing.

Is there any way to bring in a string and have it interpreted as an f-string to avoid using the .format(**locals()) call?

Ideally I want to be able to code like this... (where magic_fstring_function is where the part I don't understand comes in):

template_a = f"The current name is {name}"
# OR [Ideal2] template_a = magic_fstring_function(open('template.txt').read())
names = ["foo", "bar"]
for name in names:
    print (template_a)

...with this desired output (without reading the file twice):

The current name is foo
The current name is bar

...but the actual output I get is:

The current name is {name}
The current name is {name}

See also: How can I use f-string with a variable, not with a string literal?

7
  • 8
    You can't do that with an f string. An f string is not data, and it's certainly not a string; it's code. (Check it with the dis module.) If you want code to be evaluated at a later time, you use a function. Commented Feb 27, 2017 at 23:38
  • 20
    FYI, PEP 501 proposed a feature close to your first ideal, but it's currently "deferred pending further experience with [f-strings]." Commented Feb 28, 2017 at 0:02
  • A template is a static string, but an f-string is not a string, it's a code object, as @kindall said. I think an f-string is bound against variables immediately when it's instantiated (in Python 3.6,7), not when it's eventually used. So f-string may be less useful than your ugly old .format(**locals()), although cosmetically nicer. Until PEP-501 is implemented. Commented Oct 14, 2018 at 21:37
  • 10
    Guido save us, but PEP 498 really botched it. The deferred evaluation described by PEP 501 absolutely should have been baked into the core f-string implementation. Now we're left haggling between a less featureful, extremely slow str.format() method supporting deferred evaluation on the one hand and a more featureful, extremely fast f-string syntax not supporting deferred evaluation on the other. So we still need both and Python still has no standard string formatter. Insert xkcd standards meme. Commented Sep 16, 2020 at 6:39
  • Yeah @CecilCurry it's a royal mess. 6 years later the same awfulness still persists. On top of that, there's the craziness of requiring the eminently missable f character on your strings. Missed one f in your code 5 years ago? Now when your code triggers that ultra-rare, ultra hard to reproduce exception, your debug message just says "{ environ = }". No debug info for you! Come back 5 years! </seinfeld> Remember kids, it's a BAD idea when language designers enforce premature optimization and make you impersonate a compiler instead of doing their own job properly. Say F to fstrings. Commented Dec 3, 2023 at 19:37

16 Answers 16

70

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
Sign up to request clarification or add additional context in comments.

8 Comments

by far the best answer! how did they not include this simple implementation as a built-in feature when they introduced f-strings?
nope, that loses scope. the only reason that works is because name is global. f-strings should be deferred in evaluation, but the class FString needs to create a list of references to the scoped arguments by looking at the callers locals and globals... and then evaluate the string when used.
@user3204459: Because being able to execute arbitrary strings is inherently a security hazard — which is why the use of eval() is generally discouraged.
@martineau it should have been a feature of python so that you don't need to use eval... plus, f-string has the same risks as eval() since you can put anything in curly brackets including malicious code so if that's a concern then don't use f-strings
A small extension to this answer - you can use f'f"""{template}"""' to handle templates with newlines in them. With the current implementation you'll get something like SyntaxError: EOL while scanning string literal if your template has a newline character.
|
55

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

10 Comments

I'm going to accept this as the answer, though I don't think I'll ever actually use it in code because of the extreme cleverness. Well maybe never :). Maybe the python people can use it for the implementation of PEP 501. If my questions was "how should I handle this scenario" the answer would be "just keep using the .format() function and wait for PEP 501 to resolve." Thanks for figuring out how to do what shouldn't be done, @PaulPanzer
This does not work when the template includes something more complex than simple variable names. For instance: template = "The beginning of the name is {name[:4]}" (-> TypeError: string indices must be integers)
@bli Interesting, seems to be a limitation of str.format. I used to think f-strings are just syntactic sugar for something like str.format(**locals(), **globals()) but obviously I was wrong.
Please don't use that in production. inspect is a red flag.
I have 2 questions, why is inspect a "red flag" for production would a case such as this be an exception or would there be more viable workarounds? And is there something against the use of __slots__ here for the reduced memory usage?
|
37

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:

def template_a():
    return 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.

4 Comments

Funny, I had precisely the same snippet posted. But I retracted it because of scoping limitations. (Try wrapping the for loop in a function.)
@PaulPanzer do you maybe want to edit the question and re-include it? I wouldn't mind deleting the answer. This is a viable alternative for OP's case, It isn't a viable alternative for all cases, it's being sneaky.
No, it's fine, keep it. I'm much happier with my new solution. But I can see your point that this one is viable if you are aware of its limitations. Maybe you could add a little warning to your post so nobody can shoot their foot by using it wrong?
Nice, slim solution that works in many cases! One can even shadow the function definition with a lambda expression: template_a = lambda : f"The current name is {name}"
33

How about:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'

3 Comments

But looks like you can't defer computation, e.g.: '2 * {x} = {2 * x}'.format(x=2), can you? => KeyError: '2 * x'
You can, it's just not as pretty. '2 * {x} = {y}'.format(x = 2, y = 2 * x) or '2 * {} = {}'.format(x, 2 * x)
For if we can do the same 'in {nb} day{nb_over_1}'.format(nb=4, nb_over_1='s' if 4 > 1 else ''). I can live with not being pretty but I am annoyed because I save the dict passed to .format() in db so I have a stupid field that can be recomputed. I choosed to write 'in {nb}d' or the less pretty 'in {nb} day(s)'.
22

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.

7 Comments

Agree with this answer wholeheartedly. This use case is what I was thinking about when searching for this question.
This is the correct answer. Some timings: %timeit log.finfo(f"{bar=}") 91.9 µs ± 7.45 µs per loop %timeit log.info(f"{bar=}") 56.2 µs ± 630 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each) log.setLevel(logging.CRITICAL) %timeit log.finfo("{bar=}") 575 ns ± 2.9 ns per loop %timeit log.info(f"{bar=}") 480 ns ± 9.37 ns per loop %timeit log.finfo("") 571 ns ± 2.66 ns per loop %timeit log.info(f"") 380 ns ± 0.92 ns per loop %timeit log.info("") 367 ns ± 1.65 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
"if there's a bug in the string converter..." -- the bug is it doesn't accept double quotes in the string. f_string.replace('"', '\\"') works for escaping quotes, but not for already escaped quotes (eg. if you're logging outputs).
Can't edit my comment: using 'f"""' + fstr + '"""' instead helps.
@Jaleks It looks like finfo() is always evaluated regardless of setLevel() and also always slower. Could you confirm ?
|
13

An f-string is simply a more concise way of creating a formatted string, replacing .format(**names) with f. If you don't want a string to be immediately evaluated in such a manner, don't make it an f-string. Save it as an ordinary string literal, and then call format on it later when you want to perform the interpolation, as you have been doing.

Of course, there is an alternative with eval.

template.txt:

f'The current name is {name}'

Code:

>>> template_a = open('template.txt').read()
>>> names = 'foo', 'bar'
>>> for name in names:
...     print(eval(template_a))
...
The current name is foo
The current name is bar

But then all you've managed to do is replace str.format with eval, which is surely not worth it. Just keep using regular strings with a format call.

4 Comments

I really see no advantage in your snippet of code. I mean, you can always write just The current name is {name} inside the template.txt file and then use print(template_a.format(name=name)) (or .format(**locals())). The code is about 10 characters longer, but it doesn't introduce any possible security issues due to eval.
@Bakuriu - Yes; like I said, although eval does allow us to write f'{name}' and delay the evaluation of name until desired, it is inferior to simply creating a regular template string and then calling format on it, as the OP was already doing.
"An f-string is simply a more concise way of creating a formatted string, replacing .format(**names) with f." Not quite - they use different syntax. I don't have a recent-enough python3 to check with, but for example I believe f'{a+b}' works, while '{a+b}'.format(a=a, b=b) raises KeyError. .format() is probably fine in many contexts, but it's not a drop-in replacement.
@philh I think I just encountered an example where .format is not equivalent to an f-string, that can support you comment: DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"{DNA[2:8]}"; failed_fragment = "{DNA[2:8]}".format(**locals()). The attempt to create failed_fragment results in TypeError: string indices must be integers.
13

What you want appears to be being considered as a Python enhancement.

Meanwhile — from the linked discussion — the following seems like it would be a reasonable workaround that doesn't require using eval():

class FL:
    def __init__(self, func):
        self.func = func
    def __str__(self):
        return self.func()


template_a = FL(lambda: f"The current name, number is {name!r}, {number+1}")
names = "foo", "bar"
numbers = 40, 41
for name, number in zip(names, numbers):
    print(template_a)

Output:

The current name, number is 'foo', 41
The current name, number is 'bar', 42

4 Comments

this workaround still does not address the fundamental limitation. for example, it still does not allow use of external code defined in a configuration file, as specified in the original question magic_fstring_function(open('template.txt').read())
@dreftymac: A regular f-string does not support passing the contents of a text file to it like that — so it's unclear exactly what point you're trying to make…
No point in particular, just noting that the ideal case requested in the original question is not satisfied by this approach. This will save time for anyone who reads this answer with the mistaken impression that it addresses that facet of the question.
"doesn't require using eval()" how is this any safer than user3204459's answer which uses eval()?
10

Or maybe do not use f-strings, just format:

fun = "The curent name is {name}".format
names = ["foo", "bar"]
for name in names:
    print(fun(name=name))

In version without names:

fun = "The curent name is {}".format
names = ["foo", "bar"]
for name in names:
    print(fun(name))

2 Comments

This does not work in all cases. Example: fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA). -> TypeError: string indices must be integers
But it doesn't work also in normal usage, please look at answer stackoverflow.com/questions/14072810/…
10

inspired by the answer by kadee, the following can be used to define a deferred-f-string class.

class FStr:
    def __init__(self, s):
        self._s = s
    def __repr__(self):
        return eval(f"f'{self._s}'")

...

template_a = FStr('The current name is {name}')

names = ["foo", "bar"]
for name in names:
    print (template_a)

which is exactly what the question asked for

Comments

5

Most of these answers will get you something that behaves sort of like f-strings some of the time, but they will all go wrong in some cases. There is a package on pypi f-yeah that does all this, only costing you two extra characters! (full disclosure, I am the author)

from fyeah import f

print(f("""'{'"all" the quotes'}'"""))

There are a lot of differences between f-strings and format calls, here is a probably incomplete list

  • f-strings allow for arbitrary eval of python code
  • f-strings cannot contain a backslash in the expression (since formatted strings don't have an expression, so I suppose you could say this isn't a difference, but it does differ from what a raw eval() can do)
  • dict lookups in formatted strings must not be quoted. dict lookups in f-strings can be quoted, and so non-string keys can also be looked up
  • f-strings have a debug format that format() does not: f"The argument is {spam=}"
  • f-string expressions cannot be empty

The suggestions to use eval will get you full f-string format support, but they don't work on all string types.

def f_template(the_string):
    return eval(f"f'{the_string}'")

print(f_template('some "quoted" string'))
print(f_template("some 'quoted' string"))
some "quoted" string
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f_template
  File "<string>", line 1
    f'some 'quoted' string'
            ^
SyntaxError: invalid syntax

This example will also get variable scoping wrong in some cases.

4 Comments

Wow, super. Works out of the box. Hats off to that 11-rep man! Like your list of differences, inspires confidence. Any gotchas you encountered? I see you developed with a (small) test suite. To be honest I have no idea what you are doing in your c file (_cfyeah.c) there ... but it looks like you know what you're doing.
Hey thanks! Definitely tried to make it easy to use, so that's good to hear. the _cfyeah.c is exposing the native CPython fstring eval, which is not part of the public Python API. It's not necessary to the package, but provides a large speedup if used compared to compiling a string every time.
return eval(f"""f'''{the_string}'''""") would solve some of the issues
@alex.forencich Thanks for this. It solved my problem using a multiline string.
5

to do that I prefer to use fstring inside a lambda function like:

s = lambda x: f'this is your string template to embed {x} in it.'
n = ['a' , 'b' , 'c']
for i in n:
   print( s(i) )

Comments

3

There's a lot of talk about using str.format(), but as noted it doesn't allow most of the expressions that are allowed in f-strings such as arithmetic or slices. Using eval() obviously also has it's downsides.

I'd recommend looking into a templating language such as Jinja. For my use-case it works quite well. See the example below where I have overridden the variable annotation syntax with a single curly brace to match the f-string syntax. I didn't fully review the differences between f-strings and Jinja invoked like this.

from jinja2 import Environment, BaseLoader

a, b, c = 1, 2, "345"
templ = "{a or b}{c[1:]}"

env = Environment(loader=BaseLoader, variable_start_string="{", variable_end_string="}")
env.from_string(templ).render(**locals())

results in

'145'

1 Comment

Don't forget about ssti. Once templated don't give this string another time to a template engine or you might have big issues.
2

I found this problem quite interesting and wrote my own library implementing lazy f-strings.

Install it:

pip install fazy

And use:

import f

number = 33
print(f('{number} kittens drink milk'))

This solution is well suited, for example, for logging. Read more about the features and limitations in the documentation at the link.

Comments

1

A suggestion that uses f-strings. Do your evaluation on the logical level where the templating is occurring and pass it as a generator. You can unwind it at whatever point you choose, using f-strings

In [46]: names = (i for i in ('The CIO, Reed', 'The homeless guy, Arnot', 'The security guard Spencer'))

In [47]: po = (f'Strangely, {next(names)} has a nice {i}' for i in (" nice house", " fast car", " big boat"))

In [48]: while True:  
...:     try:  
...:         print(next(po))  
...:     except StopIteration:  
...:         break  
...:       
Strangely, The CIO, Reed has a nice  nice house  
Strangely, The homeless guy, Arnot has a nice  fast car  
Strangely, The security guard Spencer has a nice  big boat  

Comments

0

You could use a .format styled replacement and explicitly define the replaced variable name:

template_a = "The current name is {name}"
names = ["foo", "bar"]
for name in names:
    print (template_a.format(name=name))

Output

The current name is foo
The current name is bar

Comments

0

I came upon this question which was similar to something I was trying to do, the only difference is that I needed some of the variables for my f-string evaluated early and then defer other f-string variables to be determined later.

So to borrow from the original example, here's how I pulled it off:

from datetime import date
now = date.today()
names = ["foo", "bar"]

# double bracket allows `name` to be evaluated by the next format() call
template_a = f"The current name is {{name}} and the current date is {now}."

for name in names:
    print (template_a.format(name=name))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.