179

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}
martineau
  • 119,623
  • 25
  • 170
  • 301
JDAnders
  • 4,831
  • 4
  • 10
  • 8
  • 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. – kindall Feb 27 '17 at 23:38
  • 18
    FYI, [PEP 501](https://www.python.org/dev/peps/pep-0501/) proposed a feature close to your first ideal, but it's currently "deferred pending further experience with [f-strings]." – jwodder Feb 28 '17 at 00: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. – smci Oct 14 '18 at 21:37
  • 7
    Guido save us, but [PEP 498 *really* botched it](https://www.python.org/dev/peps/pep-0498/). The deferred evaluation described by [PEP 501](https://www.python.org/dev/peps/pep-0501/) 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.** – Cecil Curry Sep 16 '20 at 06:39

15 Answers15

54

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
kadee
  • 8,067
  • 1
  • 39
  • 31
  • 4
    by far the best answer! how did they not include this simple implementation as a built-in feature when they introduced f-strings? – user3204459 May 15 '19 at 13:57
  • 6
    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. – Erik Aronesty Jun 14 '19 at 13:50
  • 5
    @user3204459: Because being able to execute arbitrary strings is inherently a security hazard — which is why the use of `eval()` is generally discouraged. – martineau Nov 12 '19 at 15:58
  • 4
    @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 – user3204459 Nov 12 '19 at 20:13
  • 2
    @user3204459 That's different risk: eval allows to run an arbitrary code from a string from any - unreliable - source (file, command line, webservice, form field, etc). The f-string enclosed code can only come from the source code itself. It can still do harm of course (e.g. if you write something like f'result is { os.system( userSuppliedCommand ) }' ), but I find it easier to write safer code with it than with eval. – huelbois Mar 11 '20 at 07:22
  • 2
    This is exactly what I was looking for, ducking for 'fstr postpone". Eval seems no worse than the use of fstrings in general, as they, i guess, both possess the same power: f"{eval('print(42)')}" – user2692263 Aug 09 '20 at 09:58
  • 4
    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. – Max Shenfield Feb 15 '21 at 21:02
48

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
martineau
  • 119,623
  • 25
  • 170
  • 301
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • That does allow the template to be defined elsewhere, but not as a string. One way I do templates is as a file full of `{format}` markers. Can this solution be adapted to a case where the template is coming from an `open('file').read()` (without actually re-reading the file every time, hopefully)? I'll edit the question to clarify. – JDAnders Feb 27 '17 at 23:34
  • @JDAnders Please have a look at the latest and greatest version which I'd say implements your "Ideal 2" to the comma. – Paul Panzer Feb 28 '17 at 07:20
  • 25
    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](https://www.python.org/dev/peps/pep-0501/). 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 – JDAnders Feb 28 '17 at 20:01
  • 9
    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 Feb 05 '18 at 16:50
  • 6
    @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. – Paul Panzer Feb 05 '18 at 16:57
  • 5
    Please don't use that in production. `inspect` is a red flag. – alexandernst Jan 05 '19 at 10:36
  • 2
    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? – Jab Feb 16 '19 at 05:53
  • This is much better answer: https://stackoverflow.com/a/54701045/207661 – Shital Shah Apr 08 '20 at 06:02
  • Thanks for this, it was helpful for something I wanted to do: https://gist.github.com/wware/c5b97bdb6b458a4f2ff54d68cb6884f6 – Will Ware Oct 13 '22 at 12:08
33

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.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
  • 1
    Funny, I had precisely the same snippet posted. But I retracted it because of scoping limitations. (Try wrapping the for loop in a function.) – Paul Panzer Feb 28 '17 at 05:34
  • @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. – Dimitris Fasarakis Hilliard Feb 28 '17 at 05:38
  • 1
    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? – Paul Panzer Feb 28 '17 at 05:48
19

How about:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'
Denis
  • 940
  • 12
  • 16
  • But looks like you can't defer computation, e.g.: `'2 * {x} = {2 * x}'.format(x=2)`, can you? => `KeyError: '2 * x'` – Denis Apr 28 '22 at 12:55
  • 2
    You can, it's just not as pretty. `'2 * {x} = {y}'.format(x = 2, y = 2 * x)` or `'2 * {} = {}'.format(x, 2 * x)` – cowlinator Jul 26 '22 at 03:15
17

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.

Erik Aronesty
  • 11,620
  • 5
  • 64
  • 44
  • 2
    Agree with this answer wholeheartedly. This use case is what I was thinking about when searching for this question. – justhalf Aug 27 '19 at 16:37
  • 2
    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) ` – Jaleks Jul 29 '20 at 19:38
  • _"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). – admirabilis Feb 09 '21 at 15:17
  • 1
    Can't edit my comment: using `'f"""' + fstr + '"""'` instead helps. – admirabilis Feb 09 '21 at 15:31
  • Interesting approach, where is `args` used in `__deferred_flog()` ? Btw, could it be embedded into a proxy class that would supersede the original `.debug()` to `.critical()` functions ? And that would work globally across several modules as well ? – Kochise Sep 08 '21 at 14:45
  • @Jaleks It looks like `finfo()` is always evaluated regardless of `setLevel()` and also always slower. Could you confirm ? – Kochise Sep 08 '21 at 14:54
14

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.

TigerhawkT3
  • 48,464
  • 6
  • 60
  • 97
  • 5
    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 Feb 28 '17 at 07:09
  • @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. – TigerhawkT3 Feb 28 '17 at 08:04
  • 4
    "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 Jul 26 '17 at 20:08
  • 2
    @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`. – bli Feb 05 '18 at 16:36
11

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
martineau
  • 119,623
  • 25
  • 170
  • 301
  • 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 Jun 22 '22 at 11:21
  • @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… – martineau Jun 22 '22 at 21:09
  • 1
    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. – dreftymac Jun 22 '22 at 23:49
  • "doesn't require using eval()" how is this any safer than [user3204459's answer](https://stackoverflow.com/a/56152200/4531164) which uses `eval()`? – Jacob Pavlock Aug 31 '22 at 09:51
9

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

user3204459
  • 350
  • 2
  • 10
7

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))
msztolcman
  • 397
  • 3
  • 10
  • This does not work in all cases. Example: `fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA)`. -> `TypeError: string indices must be integers` – bli Feb 05 '18 at 16:42
  • But it doesn't work also in normal usage, please look at answer https://stackoverflow.com/questions/14072810/slicing-strings-in-str-format – msztolcman Feb 06 '18 at 17:10
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.

ucodery
  • 51
  • 2
  • 2
  • 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. – mike rodent Jul 17 '21 at 16:05
  • 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. – ucodery Jul 20 '21 at 00:24
  • 1
    `return eval(f"""f'''{the_string}'''""")` would solve some of the issues – alex.forencich Nov 09 '21 at 08:06
  • @alex.forencich Thanks for this. It solved my problem using a multiline string. – Indiana Bones Apr 20 '22 at 21:10
2

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) )
Alireza815
  • 311
  • 3
  • 7
1

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'
user1556435
  • 966
  • 1
  • 10
  • 22
1

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.

Evgeniy Blinov
  • 351
  • 3
  • 3
0

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  
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
Stevoisiak
  • 23,794
  • 27
  • 122
  • 225