146 comments

[ 2.7 ms ] story [ 199 ms ] thread
I can't say I like the f" syntax much. I think JS's backtick is better.

Other than that, I agree, much better than % and less verbose than .format.

It's just the f" or f' that doesn't sit right with me

It's consistent with other strings in Python like r-strings and the older u-strings.
Yup, especially as you often need to access dict's with foo["field"] and you get a syntax error if the f-strings is also f"". JavaScript backtick syntax doesn't lead to this extra syntactic friction.
You can use any of

    f"{foo['field']}"
    f'{foo["field"]}'
    f"""{foo["field"]}"""
(I see from your carefully worded first sentence that you already knew this but still find it annoying, but I'll leave this here for any that aren't aware.)
Yes, and anyone who has written a "one-liner" at the command line is familiar with it. Wouldn't say it is a big problem.
The problem with this is when you run it through a formatter that changes all ' to ".
Sounds like a bug in your formatter, if it's meant to just do formatting but actually replaces valid syntax with invalid.
I don't totally disagree. My company uses the black formatter, which does this[0]. There are flags to skip string formatting, but is frowned upon at my organization.

[0] https://github.com/psf/black/blob/master/docs/the_black_code...

For what it's worth, black appears to do a reasonable thing and preserves the semantics of your quoted strings (as is promised by the documentation):

  $ echo "'foo'" | black -
  "foo"
  
  $ echo "'foo[\"bar\"]'" | black -
  'foo["bar"]'

  $ echo "'foo[\"bar\"]+\\'baz\\''" | black -
  "foo[\"bar\"]+'baz'"
This is easily avoidable in python by using single quotes for the outer string and double quotes for the inner string, or vice versa. Or, use triple quotes for the outer string.
Yeah, if you get to the level of nesting where having available quotes is an issue, you really need to break it up into multiple expressions.
I'm surprised it works like that in Python. C# has similar interpolated strings but there it's legal to embed quotation marks within the curly brackets. Though I guess it's less necessary in Python since you can just use the other type of quote inside.
This is because of how f-strings were initially implemented: I piggy-backed them off of "regular" strings, then post-parsed inside that string. But the restriction is that the entire f-string (after the f) needs to be a valid regular Python string.

Since this:

"this is a "regular" string"

isn't valid, then neither is this:

f"dict lookup: {d["key"]}"

However, we've talked about changing f-string parsing from a "post-process regular strings" mode, to instead having the tokenizer and parser themselves aware of f-strings. When we do that, the f-string example above will work.

(edit for formatting)

It can feel annoying sometimes, but it's also nice that it prevents you from getting overly complicated with your string interpolations. Human readability is highly valued in Python culture, and things start getting hard for humans to quickly parse when you allow arbitrarily complex interpolation expressions. Assigning your complicated expression to a well-named variable first will usually make your code more readable, even if it's slightly more verbose.
I agree it's not the most beautiful, but it was the best choice given the requirements. Basically, it had to be explicit, concise, use unused ASCII syntax, and not be too cryptic or incompatible. Other quote types or backticks were not available or an option.

(I spent a lot of time thinking about it, and put the idea forward on python-ideas. Though string interpolation is nothing new of course.)

I think you are right to prefer JS's backtick notation, but not in your reason.

The main problem with python's f" syntax is that it's just a very complex, non-extensible hack whereas in javascript you can prefix a tag to specify how the interpolation happens.

Instead of this, f-strings have a lot of hardcoded and often broken behavior which can't be fixed let alone customized. For example, you can specify a field width, but it will not even work properly even in a fixed-width context, because of quite common things like emojis or CJK or combining characters don't have their width computed correctly.

It's not a hack or broken. The formatting language existed before f-strings and is somewhat a separate issue.

I'm not sure I expect it to handle every Unicode nuance either, when there are modules in the stdlib for that.

Yes. Python in general has gone from being “executable pseudo code” to a series of complex language design trade offs largely because it had failed to grasp the right abstractions in its evolution. Backtick functions in JS are so powerfully clever and basically impossible to recreate in Python.
> Python in general has gone from being “executable pseudo code” to a series of complex language design trade offs

Agree with this 100%.

> because it had failed to grasp the right abstractions in its evolution.

I'd be interested to know what you think the language's designers should have done differently?

Let’s go all the way back to `with`. What does it do? It encapsulates try/finally blocks and only try/finally. That is a weirdly specific thing to encapsulate. Why did they do that? Well, it turns out that making an anonymous block syntax that is compatible with Python’s grammar is hard, so you can’t have the equivalent of this JavaScript:

    WithOpen(filename, f => {
        // block
    })
Because blocks aren’t viable, problems have to be solved on a case-by-case basis, which is hard to do well and leaves gaps.
> Instead of this, f-strings have a lot of hardcoded and often broken behavior which can't be fixed let alone customized

They are customizable on an object-to-be-formatted basis (__format__ special method).

Yes, you can work around some of the defects I mentioned by defining your own wrapper classe(s) and doing something like this:

     f"Article: {fixformatting(somestr):20} Price: {fixformatting(price):5.3}"
Apart from somewhat negating the main advantage of f-strings (relative conciseness) it does not allow you take into account surrounding string context and of course will not fix problems when you have values that contain objects that already have __format__ defined. Also, python being python, you now slowed everything down by an order of magnitude.
I've put together a proposal to have all strings be f-strings by default.

Then you could drop the "f" prefix. I also propose adding a "p" prefix (for "plain") to remove the f-string parsing behavior.

I talked about it at the last Python core sprint, and it only got lukewarm support, so I'm not sure I'm going to pursue it.

I would personally be quite against that. That's a pretty large breaking change, with no clear benefit. It doesn't make the feature more discoverable, since you still need to know that using "{}" will cause interpolation. I'd say net zero effect on readability; mostly just individual preference. And potential negative impact on performance.
There would be a __future__ import to turn it on, at least for some number of releases. But yeah, I'm not sure it should ever be the default behavior. It makes most sense to me as a per-module opt-in, but then a __future__ import doesn't make sense. And we've resisted having per-module pragmas, so maybe it's dead in the water.
> And we've resisted having per-module pragmas, so maybe it's dead in the water.

There's a hack for that (no apologies for the self promotion; the project is a just funnin')

https://github.com/boothby/dissert

Certainly this will lead to rather confused programmers.

F-strings certainly seem like opt-in behavior to me.

> I've put together a proposal to have all strings be f-strings by default.

Please, no.

I mean, it would be a breaking change, which needs a really compelling case, will make all introductory material wrong, and...offers what benefit?

I think that would be a pretty serious harm to Python as a language. When I was learning Python, I was using PHP, which has $ interpolation for double quoted strings but not single quotes. I didn’t understand how PHP worked at all and it really confused me. In retrospect, I don’t know why it was so hard to wrap my head around, but it was. Learning Python was liberating because I knew exactly when and how interpolation would take place. The decision not to follow the lead of Perl and Bash and auto-interpolate was made deliberately by Guido many years ago. It would be a shame to go back on that decision now.
> I've put together a proposal to have all strings be f-strings by default.

Please don't do that. Seriously.

There is this trend in the Python community of blurring the line between a plain string and a formatting operation on that string, but there is a fundamental difference between the two. Instead of trying to ignore that difference, the language should make the difference explicit. "Explicit is better than implicit" is one of the things I like so much about Python. Please don't throw that away.

There is and should be a clear and fundamental difference between a plain string and a string formatting operation. The plain string is clearly the more fundamental of the two, so it should be the default.

And then there's the whole issue of breaking code with such a change.

Again, please don't do it. Don't change the language in such a fundamental way.

Won't happen. This would be a breaking change that would break pretty much every usage of `str.format`. Releasing a new incompatible version of a language is unlikely considering Python 2 to Python 3 migration mess.
(comment deleted)
I quite like the idea of using double-quotes for format strings and single-quotes for normal strings.

This isn’t sufficient on its own, though - what about raw strings? Perhaps use “” for format strings, ‘’ for raw strings and get rid of normal strings altogether?

F-strings have been around for years and this article is at least a few years old. I'm curious why this is coming up now.
People finally forced to move off Python 2.7?
Not mentioned in the article, but with Python 3.8, f-strings have gained the ability to print debug information using `=` specifier.

    name = 'world'
    print(f'hello {name.upper()=}')
outputs:

    hello name.upper()='WORLD'

It's small, but very useful in debugging an issue or logging something to the console.

https://docs.python.org/3/whatsnew/3.8.html#f-strings-suppor...

That's something I didn't know, but will know be using.
This is neat! I can think of several instances where this would be useful in my code
PHP: Am I joke to you?
> PHP: Am I joke to you?

JFYI meme style responses are frowned upon at HN. That's why you get downvoted.

A much better rephrase of the above would be something along the lines of:

PHP/Javascript/.. also have this feature since ver X. It's main difference/advantage/disadvantage is ...

> However, once you start using several parameters and longer strings, your code will quickly become much less easily readable [...] `"Hello, %s %s. You are %s. You are a %s ...`

Minor correction: % formatting allows specifying names for dictionary keys too:

    In [1]: "%(a)s and %(b)s" % {"a":"A", "b":"B"}
    Out[1]: 'A and B'
In fact I had never switched to using str.format as it just never added enough benefit for me over the old %-based formatting.

But I do like f-strings and am using them quite a bit. Calling functions or methods inside f-strings is something I had no idea was possible. That is pretty neat:

    >>> name = "Eric Idle"
    >>> f"{to_lowercase(name)} is funny."
    'eric idle is funny.'

   f"{name.lower()} is funny."
The article demonstrates both methods and functions. I just pasted the function example, but without the showing the function definition itself as it's fairly obvious :-)
I have mixed feelings about code inside f-strings. It seems like a bit of a slippery slope towards losing track of what's happening. I have been tempted to write things like below because I really just need it for the print.

    f"Info print for item  {hex(do_some_parsing(do_some_conversion(value)))}"
Which runs perfectly fine, but at least on my editor syntax highlighting stops working as it is all part of the string. And with that it becomes increasingly hard to work out what is actually being called. Now I resolved myself to pretty much just have one function call and no nesting. Things like f"{str.lower()}" or f"{hex(value)}" are great and I use them a lot.
I don’t think it’ll be an issue. This isn’t exactly new ground. We know the results from other languages. There might be some such abuse, but there’s a lot of other ways to abuse expressions and they don’t happen often because it’s just not worth it.
(comment deleted)
I have mixed feelings about just grabbing variables inside strings. Prior to this I had a coworker that really liked using the convention:

  "{value}".format(**locals())
I thought it was clever at first, but this made refactoring really hard because you would move around these strings or variables and lose track of what's happening. Maybe IDEs are better at helping with this in recent years?
If I apply a bunch of functions to a variable before printing I tend to use .format() and do it inside the format call, e.g “{}”.format(a.lower().upper())

Something just feels more right about that. Maybe it is the code highlighting that works better.

PyCharm/intellij display the “code” part of f-strings similar to how a variable declaration or function call would be. I’m sure it’s possible for any other editors out there to apply some nicer formatting to f-strings as well.
Swift's string interpolation is usually fine, tbh.
This reminds me of Swift's string formatting syntax:

  let name = "Bob"
  print("Hi my name is \(name)")
Which I always quite liked. I found myself missing this when I moved on to Rust and Go.
Kotlin is even simpler with statements like "hello $variable" or "hello ${function()}"
This overloads the backslash \().

Which is usually used for escaping characters. Like, \n for new line.

So, you may end up with code like:

  print(“Hi. \nMy name is \(name).”)
And do you think that’s good or bad?

I think it’s nice that there is only one escape character. You check for ‘\’. If it’s there, switch on the next character checking for ‘n’, ‘t’, ‘r’ or ‘(‘, defaulting to appending the unmodified character to the output.

I think that might not only be simpler, but also be slightly faster than having having two different escapes would be.

I think the intent here is that you are escaping into an interpolation context…
A couple of years ago I wrote some Python code which took advantage of this syntax. Then it turned it had to run in an environment without it.

I wrote a simple Awk script to convert Python with f-string syntax to syntax without it.

(The program had a function to do a more or less proper lexical analysis job; not some flimsy regex substitution hack.)

The thing which annoys me most about f strings is I can't build them at runtime, which means I can't just learn/teach f strings.

While I know why building f strings is forbidden (they can run arbitrary code), I'm happy to take the risk.

Doesn't calling .format on a string do the exact same thing as an f-string?
No, because f strings can run arbitrary python code, you can write f"{X} - {y} = {X - y}"
I mean it's Python, so you can do anything:

    >>> fstring="Hello, {\"world\"}"
    >>> eval(f"print(f'{fstring}')")
    Hello, world
That doesn't work (easily) in a function as eval only has the global context, not access to local variables (although you can do some hackery to make it work)
Delayed formatting was brought up in the PEP but was rejected. That made me a little sad.
When it comes to string formatting I have recently found my absolute favourite utility of all time: Scheme's SRFI-166. It is a combinator based string formatting utility that is user extensible (with verbosity worthy of being in every scheme!): https://srfi.schemers.org/srfi-166/srfi-166.html
This is super minor but I really wish f-string formatting were the default behavior for `print` or that there were a `printf` command just for it. Typing that extra f character before the quotes is just annoying enough
And while they're at it, bring back the print statement.

It was a beautiful thing and made it possible to write the same "hello world" program in Python and BASIC:

    print "hello, world"
As a novice Python scripter, I felt that way for about 6 months. Then grumbled but accepted why making print a function was a good thing. Then spent another year frequently forgetting the brackets. Then another 6 months occasionally forgetting the brackets. Now, about 2 years after I converted to Python3, I finally have fully accepted that having print as a function is a good universal thing. At the very least, having an "end=" and "sep=" option eliminated 10,000 newbie python questions a year.
Yeah it's a backwards compatibility issue, wouldn't be surprised if they make this change in Python4
I just find the convergent evolution funny, that my C fingers that could type `printf()` fluently now learn to type `print(f"")` the same way
So far Ruby provided me the best user experience regarding string interpolation, and it owes a large part of it to Perl.

Squiggly heredoc is something I am unaware of in any other general programming language.

Elixir heredocs do this by default. Plus you can prepend it with sigils to activate string macros. Here I use it to inject content into another programming language embedded within Elixir (lower case ~z allows interpolation, upper case ~Z does not):

https://www.youtube.com/watch?v=fIxNEILcNGM&t=25m50s

I learned this nice syntax from a tweet yesterday:

    >>> import datetime
    >>> now = datetime.datetime.now()
    >>> f"Today is {now:%m/%d/%Y}."
    'Today is 01/20/2021.'
No need for now.strftime() in simple string output!

https://twitter.com/mariatta/status/1351359518316216321

Thank you! This was literally what I opened the article for (I'd seen the f syntax previously but hadn't paid much attention) and it's conspicuously missing!
I love this - didn't realize it was a thing at all! If anyone is curious, this works for any type in Python that implements __format__ ({now:%m/%d/%Y} is equivalent to {now.__format__('%m/%d/%Y')}), and you can (ostensibly) make the format string mean whatever you want.
While I really do like this, I cannot avoid remarking that python starts to look more and more like perl
I found out about f strings from an HN comment a few years ago. Ever since I have been super excited to show people who don't know about them.

In Python 2 I used to use .format(**locals()) which is basically the same thing, but felt like a hack.

The more efficient version of this is .format_map(locals())
I didnt know format_map existed, but it looks like it is not in python 2 so it wouldn't have helped in that use case. Still, thanks for pointing that out. I love learning about new minor conveniences in Python.
I saw this a few years ago and thought it was cool at first, but I had huge problems when refactoring that code. I would miss variables used in strings when I would rename, move, or delete things. Have you had this issue? I imagine an IDE that could grok it would have helped.
Pycharm "refactor" for variables has the option to search for the variable-name being changed in comments and strings.

But for f-strings, it has full support for the "inline" variables.

> But for f-strings, it has full support for the "inline" variables.

Is this complete (meaning: it also includes more complicated expressions, referencing these variables)?

I'm asking, because some Python development tools are failing in doing it right. This leads to annoying reliability issues for refactorings (even for simple renames). Currently, I have the problem with Wing IDE.

As far as I know yes. It knows that the "usage" of that variable in an f-string is an actual reference to the variable vs just part of the string with the same set of characters.

Not sure what you mean by complicated expressions? If you mean the variable is referenced in a complicated statement with calcs or as part of a ternary operator, etc... AFAIK they are all supported. It even understands scope. So it won't go outside of the current scope where the variable is defined.

PyCharm's refactoring is very neat. In certain instances it even "refactors" relative paths inside a string if you move the file being referenced. Moving files around is intelligent and all imports get fixed. It has neat naming-convention renaming (myVariable->my_variable). Lots of nice little quality-of-life features, which is why I think it is the best Python IDE out there atm. Give it a whirl, it's got a free community edition that is near 100% functional!

I tried it (again). First impression: No support for the black formatter. We have 2021 - really?

https://youtrack.jetbrains.com/issue/PY-39750

Black is hardly the industry-wide standard, so I don't see why they would embed it into their IDE? Either way, the IDE has all the sufficient tools to enable me to easily call the black formatter along with pylint, pep8, flake8 and mypy. I have all of them setup in my IDE and trigger them with a right click command, a shortcut button, or automatically on save (if I wanted to).

https://www.jetbrains.com/help/pycharm/configuring-third-par...

(comment deleted)
F-strings are great. The only downside I’ve found is they are incompatible with the stdlib logging framework’s deferred string interpolation feature (using %s style string interpolation), which can give you a nice performance boost if you make lots of expensive debug logs for tests, but run at info level in production.

That said most apps probably won’t see much of a difference in perf between the two approaches.

f-strings are so much faster that I've seen an increase in speed over %-formatting for logging. If there's even a 10% chance that a string will be actually logged (and therefore %-formatting will take place), I'll replace it with an f-string and not care about the deferred formatting.
Something I find useful, and perhaps isn't widely known is that you can pop curly braces into the format part of an f-string to have dynamic formatting:

    value = 5/6
    precision = 2
    print(f"{value:.{precision}f}")
    
    > 0.83
This is great, had been hacking around this with formatfor years. Will incorporate this soon.
I have a love-hate relationship with f-strings.

They are nice to use. But they are also the single feature that I'm most likely to use that means I can't use pypy to run my code many times faster.

I wish that pypy supported them.

Upon reading the title, I was not enthusiastic. Yet another way to format strings? What's wrong with String.format()? Get off my lawn!

However upon reading the article, it does feel more ergonomic to prepend your string literal with an 'f', and have it automagically reach out into the surrounding scope for the variables to format into the string.

I do worry a bit however about formatting not just expressions, but being able to call other code. Yes, that could be quite convenient for keeping the code concise and avoiding the kind of verbosity that confuses. However, I could also see someone forgetting to sanitize user-input data, and combined with the f-string, creating a remote arbitrary code-execution vulnerability. Sure, an attacker can't immediately start scribbling on memory in Python, there's more work he has to do first. But, he possibly could hijack the Python process to run whatever Python code he wanted.

Executing user-provided code isn't possible. f-strings must be literals, so there's no user-defined strings involved.

You could of course eval something the user provided, but there's no more of a risk with f-strings than there is with regular code.

This is addressed in PEP 498.

I love f-strings. Don't forget that you can do fr"some string {blah}" to build raw strings.

  >>> blah = "something \here"
  >>> fr"what the {blah}"
  'what the something \\here'
What version of python are you using. With python 3.8.5

1. \h doesn't get interpolated to anything (unlike \a, \b, \f, etc...)

   "\h"
   '\\h'
   
   "\a"
   '\x07'
2. The "r" needs to be in the original definition, not the f-string.

    blah = r"\a"
You only need the "r" in the f-string if you don't want interpoolation of the "\" - I.e.:

   fr" this\a {blah}"
   'This\\a \\a
This was just an example to show what it does, I normally wouldn't have \h in a string. I use this to store lists of regular expressions in aws dynamodb list type and to convert them back to raw strings when used.
A wart is the standard logging module, which uses %-style strings. For example:

log.debug("Unexpected, got %r", (got,))

By doing it this way, it can defer rendering the string until it is actually needed, which is (assumed to be) a win when the logging module is normally just going to toss the low level messages away rather than emit them anywhere.

I think you lose this optimization if you use f-strings. I don't think rendering of the string can be lazy, deferred until actually needed.

Whilst percent strings can't, calls to format can.

So:

    log.debug("Unexpected, got {got}".format(got=got))
Or, to just use the available locals to automatically fill that in for you:

    log.debug("Unexepected, got {got}".format(**locals()))
Or if you want both globals and locals with a preference for locals:

    log.debug("Unexepected, got {got}".format(**{**globals(), **locals()}))
Yes, I was asked by a Junior if they should always use f strings now. There are still a couple situations where you need format, such as when you’re formatting a variable instead of a literal.

Also, shoutout to Template for untrusted strings. It’s surprisingly unknown. Most people suggest replace or a custom format function.

Also, remember that the first argument to the logging functions is considered to be a format string. So using f-strings is not just less efficient, it is also not robust, unless you do:

    logging.debug("%s", f"Unexpected, got {got}")
because otherwise, if the format string is the first/only argument, and `got` unexpectedly contain `%`, an exception will be raised due to a missing format parameter.

(Also, for the logging functions, you don't pass a tuple, just a normal series of arguments.)

I dont think this is true. I just tried with the following program and it runs as expected (Run on Python 3.7.9).

    import logging
    got = "%d"
    logging.warning(f"hi {got}")
    logging.debug(f"hi {got}")
Output

    WARNING:root:hi %d
    DEBUG:root:hi %d
As the sibling comment points out, it actually doesn't work that way. The first parameter is only treated as a format string _if_ there are arguments. If not, it is treated as a literal string: https://github.com/python/cpython/blob/dcea78ff53d02733ac598...
I stand corrected. Thanks.

(That's what I get for assuming consistent behavior :P)

I only found out about this quirk a few months ago immediately after I had given the same warning you had to someone else. It's extremely unintuitive.
logging.Formatter defaults to working with %-style strings but it it can be configured to use the {}-style of str.format. (As of 3.2)

You still miss out on the scoping of f-strings, and embedding function calls. But passing around what amounts to an "eval" expression is a footgun that should obviously be frowned upon.

There's also an option for $-style Template strings. But I can't recall when I've ever seen someone use that.

> In Python 3.2, the Formatter gained a style keyword parameter which, while defaulting to % for backward compatibility, allowed the specification of { or $ to support the formatting approaches supported by str.format() and string.Template. Note that this governs the formatting of logging messages for final output to logs, and is completely orthogonal to how an individual logging message is constructed.

The style option only applies to the outer format applied by the formatter (timestamp, level, file, line, etc), it does not apply to the logging calls.

Even if logging can't use f-strings, is there a reason it can't use `string.format`?

Limiting logging to `%`-formatting seems to be an inconsistency similar to supporting `%` but not `format` on `bytes` and `bytearray`.

> Even if logging can't use f-strings, is there a reason it can't use `string.format`?

the logging module predates the `.format` function.

From reading the comments in the discussion here, I'm curious why the f-string syntax is so all-over-the-place?

  - f"{foo}" uses the default format
  - f"{foo!r}" uses repr to format
  - f"{foo=}" uses a special-cased debugging introspection
  - f"{foo:formatstr}" uses the default format with a custom format string
  - f"{foo:{bar}}" uses a dynamic format string
It seems like `!r` and `=` should have been implemented with `:r` and `:=` instead?
Yeah, there seem to be a number of special cases.

The rules for the trailing `=` case are particularly complicated:

    {x=} -> "x="+repr(x)
    {x=:.2f} -> "x="+format(x, ".2f")
    {x=:} -> "x="+format(x, "")
    {x=:!s:20} -> "x="+format(str(x), "20")
    {x=:!r:20} -> "x="+format(repr(x), "20")
https://bugs.python.org/msg341732
What if your format string started with "r" or "=", or was simply just one of those characters?
!r is one of a class of conversion flags, others are !a which uses ascii(), !s which uses str().

“=” in the format-spec (the thing that comes after the “:”) is the numeric padding alignment specifier (other alignment specifiers are left “<”, right “>”, and center “^”.)

Oh, neat, so I can stack them:

  foo = 'foo'
  print(f"{foo=!r:>20}")
  print(f"{foo.upper()=!r:>20}")
The `=` you pointed out is a special case, however, and not part of the alignment specification. Sadly, this means that the above snippet is not as nicely formatted as I'd like. Still, it does seem useful for these cases:

  print(f"{x=:.2f} {dx=:.2f} {dot_product=:.4f}")
You really have to understand how it's translated into actual code, and of course it's Python, so there's some legacy syntax to take into account. A `:` just means call format(lhs, rhs), for example.

You can also actually combine those syntaxes - I often use f"{foo:%Y-%m-%d!r}" to get a quoted, properly escaped date in a specific format, for example.

ES6 spec for template literals, while not unique as far as I know, has really been the most intuitive method for templating strings that I’ve seen. It’s a bit implicit so it probably wouldn’t work for Python (in this case, % formatting wins).
Some experiences of counterparts from languages I've used, although it's like bike shedding but it's still user experience. It would be user-hostile if not carefully designed:

Ruby: Good but there are too many kinds of syntax. Use double quote to interpolate which need to escape a lot. And single quote doesn't interpolate, which makes many code bases very mixed and merging code is annoying. I wonder if they swapped them in the first place would it make the mixed thing better because it's easier to settle on single quote?

C#: Support multiline, still awkward because it uses double quote.

JavaScript: Simple, intuitive yet extensible. It's pretty useful because in JavaScript world embedded languages are very common (CSS, HTML etc). And I find ${} is slightly better than {} in C# and Scala because of the escaping. The community is pretty much settled with single quote, and only use back tick when interpolating so it suck less than Ruby.

Scala: Extensible. Very thoughtful and cool, you can indent and strip margin. It's a little bit cumbersome to write multiline when it's space sensitive in JavaScript

Elixir: Extremely extensible, the Sigil is not only for strings but also for other syntax.

Clojure: It's weird that I put Clojure here, yet I find (str "Hello" name "!") is not far from dedicated interpolation syntax. S-expr, simplicity and all that.

I would say Clojure's is good enough for me, but JavaScript is pretty awesome already - maybe it would be perfect if it can strip margin as well.

The issue with f-strings is that they can't be used for i18n. AFAIK, it is impossible to make `_(f"Hello, {name}")` work - at least without a preprocessor or very arcane bytecode manipulation magic. This is unlike much uglier but common and well-supported `_("Hello, %(name)s") % {"name": name}`.

In the examples I've used `_` as an alias for a `gettext` call - I believe this is a common practice to do something like `from django.utils.translation import ugettext as _`.

Not f-strings, but I am partial to something like:

    _("Hello, {name}").format(name=name)

    _("Hello, {name}).format(**vars())
A key feature to the recent JS and C# implementations of string interpretation ("template strings") was that both provide a way to pass a template string to a function in a deconstructed form (an array of string parts, and an array of the strings expected to fill their holes). In C# it's a static type expectation (if you cast to a string you get the final formatted string, if you request the FormattableString you get the full deconstructed meta-object) and in JS it's a special function call signature (`` is the template string delimiter and a function that takes the deconstructed parts of a template string is prefixed functionname`example string`).

This bit of additional functionality in both JS and C# makes their interpolated strings/template literals extremely useful for i18n (and other uses such as query parameterization and injection attack avoidance).

I'm surprised that in comparing the efforts of other languages Python didn't include such functionality in the f-strings PEP. I wonder if such a feature could be added in a non-disruptive way. Maybe a meta-property like a __format_parts__ or something?

Before python had F-Strings, this is one of the reasons why I prefered using Ruby. F-Strings is one of best thing to happen in Python 3.