[Python-Dev] Delayed evaluation of f-strings?

841 views
Skip to first unread message

Eric Nieuwland

unread,
Jun 24, 2021, 12:26:01 PM6/24/21
to Python-Dev
In a recent discussion with a colleague we wondered if it would be possible to postpone the evaluation of an f-string so we could use it like a regular string and .format() or ‘%’.


import inspect

class DelayedFString(str):
    def __str__(self):
        vars = inspect.currentframe().f_back.f_globals.copy()
        vars.update(inspect.currentframe().f_back.f_locals)
        return self.format(**vars)

delayed_fstring = DelayedFString("The current name is {name}")

# use it inside a function to demonstrate it gets the scoping right
def new_scope():
    names = ["foo", "bar"]
    for name in names:
        print(delayed_fstring)

new_scope()

While this does what it should it is very slow.
So I wondered whether it would be an idea to introduce d-strings (delayed f-strings) and make f-strings syntactic sugar for

f"The current name is {name}" = str(d"The current name is {name}")

And perhaps access to the variables and conversions specified in the d-string.

Thomas Grainger

unread,
Jun 24, 2021, 12:32:08 PM6/24/21
to pytho...@python.org
for your usecase I'd write:


def delayed_fstring(*, name: str) -> str:
return "The current name is {name}"

def new_scope() -> None:
for name in ["foo", "bar"]:
print(delayed_fstring(name=name))


for logging I use:

class Msg:
def __init__(self, fn: Callable[[], str]):
self._fn = fn

def __str__(self) -> str:
return self._fn()
...
logger.info(Msg(lambda: f"The current name is {name}"))
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/ZXRUDEMIY4AV3SIVIEAWHFKFMPGOLI4J/
Code of Conduct: http://python.org/psf/codeofconduct/

Martin (gzlist) via Python-Dev

unread,
Jun 24, 2021, 12:42:54 PM6/24/21
to Eric Nieuwland, Python-Dev
On Thu, 24 Jun 2021 at 17:25, Eric Nieuwland <eric.ni...@gmail.com> wrote:
>
> class DelayedFString(str):
> def __str__(self):
> vars = inspect.currentframe().f_back.f_globals.copy()
> vars.update(inspect.currentframe().f_back.f_locals)
> return self.format(**vars)

This isn't quite right as the semantics between f-strings and
str.format() are not actually the same (though this isn't well
documented):

>>> f'{1 + 2}'
'3'
>>> str(DelayedFString('{1 + 2}'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in __str__
KeyError: '1 + 2'

>>> d = dict(a=1)
>>> f'{d["a"]}'
'1'
>>> str(DelayedFString('{d["a"]}'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in __str__
KeyError: '"a"'

Basically, f-strings rely on eval-like semantics.

Martin
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/HGITYZPPRXJM4Y7YVJKUSVXUB75W5Z2L/

Martin (gzlist) via Python-Dev

unread,
Jun 24, 2021, 12:50:11 PM6/24/21
to Eric Nieuwland, Python-Dev
On Thu, 24 Jun 2021 at 17:37, Martin (gzlist) <gzl...@googlemail.com> wrote:
>
> >>> d = dict(a=1)
> >>> f'{d["a"]}'
> '1'
> >>> str(DelayedFString('{d["a"]}'))
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> File "<stdin>", line 5, in __str__
> KeyError: '"a"'

And the other side of the attribute lookup:

>>> d = dict(a=1)
>>> str(DelayedFString('{d[a]}'))
'1'
>>> f'{d[a]}'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

Yes, having three different ways of doing string interpolation (not
counting other things you can import, like string.Template) is a bit
confusing.

Martin
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/A7FQUWQND56VGHCTTCFW6XDNCWP5MVNM/

Luciano Ramalho

unread,
Jun 24, 2021, 1:08:45 PM6/24/21
to Eric Nieuwland, Python-Dev
I don't think that would be a good idea since we already have
.format() which covers that use case and is more flexible than
f-strings (it supports positional arguments, as well as *args and
**kwargs).

I think keeping f-strings simple is a better idea.

Best,

Luciano
> _______________________________________________
> Python-Dev mailing list -- pytho...@python.org
> To unsubscribe send an email to python-d...@python.org
> https://mail.python.org/mailman3/lists/python-dev.python.org/
> Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/GT5DNA7RKRLFWE3V42OTWB7X5ER7KNSL/
--
Luciano Ramalho
| Author of Fluent Python (O'Reilly, 2015)
| http://shop.oreilly.com/product/0636920032519.do
| Technical Principal at ThoughtWorks
| Twitter: @ramalhoorg
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/LVPDNGURA677ODMLBVUURPXKYGBKJ6A4/

Eric Nieuwland

unread,
Jun 24, 2021, 1:24:46 PM6/24/21
to Martin (gzlist), Python-Dev
I didn’t make myself clear, sorry.

The code example was just to give an initial what I was suggesting.
The errors you show demonstrate the example is incomplete.
I’d like them to work.
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/L5U7ES6PS5LW532PAP62CD4PU55WEBAI/

micro codery

unread,
Jun 24, 2021, 1:32:03 PM6/24/21
to Python-Dev
As pointed out already, f-strings and format are subtly different (not counting that one can eval and the other cannot). Besides quoting, the f-sting mini language has diverged from format's
>>> spam="Spam"
>>> f"{spam=}"
"spam='Spam'"
>>> "{spam=}".format(spam=spam)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'spam='

I created a package some time ago to do exactly this https://pypi.org/project/f-yeah/


Eric Nieuwland

unread,
Jun 24, 2021, 1:32:37 PM6/24/21
to Luciano Ramalho, Python-Dev
Except I like the mini-language of f-strings much better than format()’s.
And there is a performance difference between f-strings and format().
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/YJQDXGTFX7G2P6OLYT3QF3IFN7Z65FSG/

Eric V. Smith

unread,
Jun 24, 2021, 1:39:34 PM6/24/21
to pytho...@python.org

What part are you trying to delay, the expression evaluations, or the string building part?

There was a recent discussion on python-ideas starting at https://mail.python.org/archives/list/python...@python.org/message/LYAC7JC5253QISKDLRMUCN27GZVIUWZC/ that might interest you.

Eric

-- 
Eric V. Smith

Guido van Rossum

unread,
Jun 24, 2021, 11:33:47 PM6/24/21
to Eric Smith, Python-Dev
On Thu, Jun 24, 2021 at 10:34 AM micro codery <uco...@gmail.com> wrote:
As pointed out already, f-strings and format are subtly different (not counting that one can eval and the other cannot). Besides quoting, the f-sting mini language has diverged from format's
>>> spam="Spam"
>>> f"{spam=}"
"spam='Spam'"
>>> "{spam=}".format(spam=spam)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'spam='

Eric, what would you think of adding this feature to format()? It seems doable (at least for keyword args -- for positional args I don't think it makes sense).

Honestly, the rest of the discussion belongs on python-ideas.

--
--Guido van Rossum (python.org/~guido)

Serhiy Storchaka

unread,
Jun 25, 2021, 4:53:57 AM6/25/21
to pytho...@python.org
24.06.21 12:37, Eric Nieuwland пише:
> In a recent discussion with a colleague we wondered if it would be
> possible to postpone the evaluation of an f-string so we could use it
> like a regular string and .format() or ‘%’.

You can just use lambda:

delayed_fstring = lambda: f"The current name is {name}"
print(delayed_fstring())

If you want to get rid of () when convert to string, you can use a wrapper:

class LazyStr:
def __init__(self, str_func):
self._str_func = str_func
def __str__(self):
return self._str_func()

delayed_fstring = LazyStr(lambda: f"The current name is {name}")
print(delayed_fstring)

but it works only if str() is directly or indirectly called for the
value. You cannot pass it open() as a file name, because open() expects
only str, bytes, integer of path-like object. Most builtin functions
will not and could not work with LazyStr.

As for evaluating variables in different scope as in your example, it
just does not work in Python. Python uses static binding, not dynamic
binding. First than introducing "delayed f-strings" you need to
introduce dynamic binding in normal functions.

_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/IG6HEHUXY3K4WJ2PYIG47II7ESOIAQIK/

Eric V. Smith

unread,
Jun 25, 2021, 4:58:27 AM6/25/21
to gu...@python.org, Python-Dev
On 6/24/2021 11:28 PM, Guido van Rossum wrote:
On Thu, Jun 24, 2021 at 10:34 AM micro codery <uco...@gmail.com> wrote:
As pointed out already, f-strings and format are subtly different (not counting that one can eval and the other cannot). Besides quoting, the f-sting mini language has diverged from format's
>>> spam="Spam"
>>> f"{spam=}"
"spam='Spam'"
>>> "{spam=}".format(spam=spam)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'spam='

Eric, what would you think of adding this feature to format()? It seems doable (at least for keyword args -- for positional args I don't think it makes sense).

The only problem is that it already has a meaning, as unlikely as it is that someone would use it:

>>> d = {'x = ': 42}
>>> "{x = }".format(**d)
'42'

Plus there's the issue of whitespace being treated differently with f-strings and str.format().


Honestly, the rest of the discussion belongs on python-ideas.

I totally agree.

Eric

Eric Nieuwland

unread,
Jun 25, 2021, 12:21:56 PM6/25/21
to uco...@gmail.com, Python-Dev

> On 24 Jun 2021, at 10:28, micro codery <uco...@gmail.com> wrote:
>
> As pointed out already, f-strings and format are subtly different (not
> counting that one can eval and the other cannot). Besides quoting, the
> f-sting mini language has diverged from format's
>>>> spam="Spam"
>>>> f"{spam=}"
> "spam='Spam'"
>>>> "{spam=}".format(spam=spam)
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> KeyError: 'spam='
>
> I created a package some time ago to do exactly this
> https://pypi.org/project/f-yeah/

Lovely! Thanks.

—eric

_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/RIWZ6ABBMQXB32QVUCJU43QZM3HNZ74R/
Reply all
Reply to author
Forward
0 new messages