4

When using formatted string literal, it is possible to have nested f-strings to some extent.

a = 3
b = 7

res = f"{f'{a*b}'}"

print(res) # '21'

Although, the same does not work if the inner expression is a variable containing a string.

a = 3
b = 7

expr = 'a*b'

res = f"{f'{expr}'}"

print(res) # 'a*b'

Is there a way to make this work and to have the second output to be '21' as well? If not, what is the difference between the first and second string that prevents it?

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73

3 Answers3

3

There are a few libraries that have developed functions for evaluating numerical and logical expressions safely ("safe" being the key).

First, the setup -

a = 3
b = 7
op = '*'

numexpr.evaluate

>>> import numexpr as ne
>>> ne.evaluate(f'{a} {op} {b}')
array(21, dtype=int32)

numexpr is smart enough to optimise your expressions, and is even faster than numpy in some instances. Install using pip.


pandas.eval

A safe eval from the Pandas API similar to ne.evaluate.

>>> import pandas as pd
>>> pd.eval(f'{a} {op} {c}')
12
cs95
  • 379,657
  • 97
  • 704
  • 746
  • Wasn't it you on a question 10 minutes ago that took an oath to downvote anything using eval? – Olivier Melançon Jun 08 '18 at 03:52
  • 2
    @OlivierMelançon builtin `eval`, yes. And I still will – cs95 Jun 08 '18 at 03:53
  • That's fair, I am more interested in knowing *why* it cannot be done with f-strings if it actually cannot, but your answer is interesting. Why is pandas.eval considered safe? – Olivier Melançon Jun 08 '18 at 03:54
  • 1
    @OlivierMelançon Why doesn't it work? I'm not sure, but I can answer the second question. `pandas.eval` actually has code to parse expressions it receives, and it is strict about the kinds of operations it considers valid and what not. For example, import statements to `pd.eval` are a strict no-no. – cs95 Jun 08 '18 at 03:56
2

I think it can be helpful to see what is actually happening under the hood when each of these expressions is called.

f"{f'{a*b}'}"

def om1(a, b):
    return f"{f'{a*b}'}"

dis.dis(om1)
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_MULTIPLY
              6 FORMAT_VALUE             0
              8 FORMAT_VALUE             0
             10 RETURN_VALUE

The outer f-string encounters an expression which it evaluates, and the inner f-string also finds an expression which it evaluates, which results in the call to BINARY_MULTIPLY

f"{f'{expr}'}"

def om2(a, b):
    expr = 'a*b'
    return f"{f'{expr}'}"

dis.dis(om2)
  2           0 LOAD_CONST               1 ('a*b')
              2 STORE_FAST               2 (expr)

  3           4 LOAD_FAST                2 (expr)
              6 FORMAT_VALUE             0
              8 FORMAT_VALUE             0
             10 RETURN_VALUE

Here, the first f-string encounters an expression and evaluates it, and the inner f-string, encounters a string, resulting in the call to LOAD_FAST instead of evaluating the contents of the string as Python code.

Also it is important to note in this second example the missing LOAD_FAST calls to both a and b, which are present in the first example.

Community
  • 1
  • 1
user3483203
  • 50,081
  • 9
  • 65
  • 94
  • It is a good idea to display the bytecode. This answer has been really helpful, but would not have been quite sufficient if it was not for the key fact, given in @Amadan answer, that f-string code is generated at compile time, not at runtime and that it *does not* rely on calling an eval. If I could acceot both answers I would as it is combined that they give the best explanation. – Olivier Melançon Jun 08 '18 at 04:22
2

It's called "string literal interpolation". The string must be a literal, i.e. at the time the compilation takes place compiler will turn the string into a proper executable code. If you already have a string as a value (not as a literal), it's too late for that.

I don't have access to Python that has PEP 498 enabled, so my examples will be in Ruby, which has had this mechanism for a long time. The Ruby syntax for Python's f"...{expr}..." is "...#{expr}...".

In Ruby, "a#{2 * 3}b" is syntactic sugar for ["a", (2 * 3), "b"].join (as in, they produce exactly the same bytecode). If you have the string "2 * 3" already as a value, the compiler can't do anything about it; the only way to turn a string value into a result is to evaluate it.

In the first example, you have a string literal inside a string literal; both are processed by the compiler at compile time: when the compiler sees the outer literal, it compiles it, finds another string literal there, compiles that as well, produces code. As a matter of fact, "a#{"#{2 * 3}"}b" produces exactly the same byte code, again.

The fact that this is done at compile time is also the reason why string literal interpolation will raise a syntax error if the expression inside is malformed, even if the line in question is never executed: if false; "#{1+}"; end will produce a SyntaxError.

The fact that this is done at compile time means strings already in variables are not eligible for this mechanism. In your code, by the time res is evaluated, expr could have been anything; the only way out is evil (or another, safer, evaluator).

Amadan
  • 191,408
  • 23
  • 240
  • 301