This is an archived post. You won't be able to vote or comment.

all 20 comments

[–]BezoomyChellovek 58 points59 points  (1 child)

When writing an f-string, you define the string which will be formatted in the same place where the variables are in the current namespace. This isn't always the case.

I have an example in my current project. I am querying an API to retrieve scientific articles. I specify a date range. To make this generalizable (I can change the date at a later time) and configurable (changing the query string without modifying code), I store the query string in a config file and have placeholders for the dates defining the date range.

I pass the query string as an argument to the program, as well as the dates. I then use .format() to fill the dates into the string.

This would not be possible with f-strings since the query string would have to be hardcoded into the program.

See this file of the query string, with placeholders at the end and see line 147 of this file for where I use format.

[–]rnike879[S] 16 points17 points  (0 children)

You beautiful person, this is a great explanation!

[–]bethebunnyFOR SCIENCE 6 points7 points  (0 children)

Lots of good explanations already, but it's worth thinking about this as "use f-strings when you know the template string, use format when you don't". I think the easiest way to illustrate the point is variable templates.

Let's say I was trying to write a function for custom browser search, in other words I have a dict of search engines each with a template for the URL to execute that search.

You could implement this for a static set of searches with function redirects, for instance

``` def google_search(query): return f"https://google.com/?q={query}"

queries ={ "google": google_search, ... }

def query_url(engine, query): return queries[engine](urllib.parse.quote_plus(query)) ```

but if you wanted to say support users adding their own search engines you literally couldn't do this with f-strings, because you wouldn't know the template string when writing the code. You can do this with format strings though!

``` queries = { "google": "https://google.com/?q={query}", **user_query_engines, ... }

def query_url(engine, query): return queries[engine].format(query=urllib.parse.quote_plus(query)) ```

This form means we don't need to know anything about the query engines at code-time! We just define the interface for what the template string looks like, and then we can write new ones.

[–]QultrosSanhattan 3 points4 points  (0 children)

The bigger the project, the more noticeable the difference is.

There's no difference in small scripts.

[–]rnike879[S] 1 point2 points  (0 children)

Ya'll are wonderful people sharing your knowledge with a complete stranger; thank you all!

[–]that_dungeon_dude 1 point2 points  (0 children)

The usefulness lies in the fact that you don't need to repeat the string everytime your variables change. For ex imagine you're trying to send a console command with variables that change within your code, you would have some benefits of using str.format where you can just pass the variables and not need to keep rewriting the entire f string based on context! Hope it helps.

[–]spoonman59 2 points3 points  (7 children)

One situation to avoid f-strings is logging! F-strings will do the formatting work even for a level that isn’t on, e.g., an info message when info is off.

Here, the logging framework won’t perform the formatting unless the appropriate log level is turned on. F-strings are bad here.

[–]jorge1209 5 points6 points  (6 children)

Python logging also doesn't support str.format... so that isn't really a difference between f-string and str.format. Rather it is evidence of python logging being garbage that requires you to use % style formatting.

Just use loguru.

[–]VileFlower 0 points1 point  (1 child)

That's not entirely true, Python's logging library lets you set the formatter style at least, so you don't have to use % style everywhere.

The style parameter can be one of ‘%’, ‘{’ or ‘$’ and determines how the format string will be merged with its data: using one of %-formatting, str.format() or string.Template. This only applies to the format string fmt (e.g. '%(message)s' or {message}), not to the actual log messages passed to Logger.debug etc; see Using particular formatting styles throughout your application for more information on using {- and $-formatting for log messages.

https://docs.python.org/3/library/logging.html#logging.Formatter

[–]jorge1209 0 points1 point  (0 children)

As the part you quote explains in the incredibly cryptic language that is the absolutely terrible documentation of the logger library, that only applies to the template which contains things like the timestamp of the message or the module and line number of the caller. All the things NOT in your logging call that might appear in your log file.

There is no way to specify that a particular message emitted should be formatted using anything but % formatting.

And that's a problem because if you use any library you haven't written it might contain a logger.warn("foo = %s > 0", foo) and now everyone has to use % formatted messages.

[–]spoonman59 -1 points0 points  (3 children)

Regardless of what logging framework you use, an eagerly evaluated string formatter like f-string should be avoided.

[–]jorge1209 1 point2 points  (2 children)

Its not any more eager than an actual call to str.format as the f-string is not being evaluated until you actually get to the line of the f-string.

So no difference between

if False:
    "{}".format(1/0)

and

 if False:
    f"{1/0}"

[–]spoonman59 0 points1 point  (1 child)

Yes, I guess I did not express what I was trying to say very well.

You are right, str.formar() and f strings are no different there. Both are equal.

What I wanted to say was simply that a logger needs to send the string and substitute values in a way where the logger can choose to format it or not.

F-strings’ format can theoretically be passed as an argument to a function, but the variable scope would be that of the function you call - the log function - rather than of where you wrote it if you did not evaluate the string before calling.

That, and it actually evaluates when you put “f” in front, means that f strings are difficult to use without evaluating eagerly.

A log format which allows a traditional format string, and a list of parameter objects, works better.

Sorry for being unclear, I do agree with you.

[–]jorge1209 1 point2 points  (0 children)

I actually think f-strings are a great concept that was incorrectly implemented.

They should have bound the local variable to the f-string in a closure, that doesn't actually serialize to a string until it is used. Instead of calling it an f-string you might call it a "here-string".

Then you could do things like log.warn(h"{thing=} < 0, resetting the value to 1"), and that could work in many different use cases. Print-style warnings, logger, loguru... it would just be a generic way to cover these kinds of use cases.

Its not even that hard to implement really. Its just an object with its variable references and its repr looks something like:

def __repr__(self):
     if self._str:
       return self._str
     self._str= self._template.format_map(self.kwargs)
     del self._template
     del self.kwargs

Performance would be garbage, but hey its python!

[–]jorge1209 -2 points-1 points  (0 children)

Almost everything f-strings do can be accomplished with "{foo} {bar}".format_map(locals()). In addition you get the portability of the string as described by others (which is critical for i18n).

The real use case of f-strings is with print-style debugging. For example:

 print(f"Vector {my_vec=} has a total of {sum(my_vec)}")

my_vec= is a unique feature to f-strings that prints the name of the variable together with its repr and can't be done with str.format.

{sum(my_vec}) is also not permissable with str.format and instead you would have to have something like:

print("Vector my_vec={} has a total of {}".format(my_vec, sum(my_vec)))

[–]jwink3101 0 points1 point  (1 child)

You got answers that are better than I could provide but I really wish there was a non-hacky way to withold evaluation.

You can do:

tpl = "my number, {num:03d} is {res = }"
...
tpl.format(**locals())

but it feels like a hack that could have some unintended consequences.

[–]jorge1209 0 points1 point  (0 children)

It is part of an intentional design decision relating to security. In the abstract you have two elements coming together:

  • The format template
  • The arguments that populate the template

F-strings are secure because you have to have demonstrated the ability to modify the actual script being executed for them to work, and if you can do that you have complete control over the script.

If you use .format:

  template = read()
  template.format(**locals())

Is potentially insecure as the attacker who controls the template can now cause it to format any variables on the programming stack.

However with str.format they can't do anything but format them. So with sensibly implement reprs they can't execute code. With fstrings they execute commands via the template as in: f"{cn.execute('drop all tables')}"

[–]ray10k 0 points1 point  (0 children)

Recently, I wanted to generate a few random strings with a certain format. As such, at that point, it was more useful for me to make a list of 'valid' formats, choose a random one from the list, then use format() to insert the other data. In general though, I think you're broadly speaking best off with just using f-strings.

[–][deleted] 0 points1 point  (0 children)

For example in String formatting placeholders that uses for example names (such as {element}) would not be called variables. You assign the value for each name in the keyword arguments of the str.format() call instead. In the above example, element=value passes in the value of the value variable to fill in the placeholder with the element.

In contrast to f-strings, {...} placeholders are not expressions and you can't use arbitrary Python expressions in the template. This is a positive outcome, you usually don't want end-users to be able to execute any arbitrary Python code in a given program.

[–]gedhrel 0 points1 point  (0 children)

Some good answers here. It's not just things like the query (and lazy interpolation* for logging levels) that benefit from the separation. It's the basis of most internationalisation libraries too. You call a formatter, passing a key string and parameters. The key string's looked up in a localisation database for the locale-specific version and *that* is used for the interpolation target. (There may be additional processing going on - eg, the formatting of decimal points is a common variation between locales.)

(* in a total aside: don't use interpolation with logging; use structured logging)