Python 3's F-Strings: An Improved String Formatting Syntax
realpython.com
realpython.com
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...
>>> 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!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"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. "{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?Something just feels more right about that. Maybe it is the code highlighting that works better.
f"{name.lower()} is funny."There should be one-- and preferably only one --obvious way to do it.
There are many ways to format a string, and F-Strings are yet another way. String formatting is not the only area where the original ZEN no longer applies. Of course this is part of a language natural evolution, however, it does make the language more confusing for new starters. Syntax sugar coating Python may also distract from solving more systematic problems, like the lack of proper multithreading in Python.
I used to like it not because it was a great language but because it had a low barrier to entry for casual programmers.
Now it's just like a worse Ruby with a lot of momentum from the early years.
Casual programmers can use the same Python that 2.x used - with very little (yet IMO logical) differences.
You can get started just as easily and use/learn (or don't) features as you need/wish.
This also shows up when the novice programmer wants to understand other people's code. If there is only one way to do things, then all common examples of a task should use that one way. If there are multiple ways (as here), then common examples (or an open source project of interest) may use any combination of them. To understand the code, the novice programmer needs to understand all of the right ways to do something rather than one of them.
Isn't the key though, that they aren't in fact equivalent features at all? %-formatting doesn't allow for mapping names or changing the order of variables. f-strings are just a shorter version of .format() and there's no conceptual difference in usage.
> If there is only one way to do things, then all common examples of a task should use that one way. If there are multiple ways (as here), then common examples (or an open source project of interest) may use any combination of them.
Unfortunately, despite all noble efforts of avoiding this, Python is already full of these. This is true for any sufficiently powerful language. That's why there's idiomatic ways (here: "the Pythonic way") of doing things and "freestyling":
x = []
for i in range(4):
if i % 2 == 0:
x.append(i)
y = []
for i in range(0, 4, 2): y.append(i)
z = [i for i in range(4) if i % 2 == 0]
r = [i for i in range(0, 4, 2)]
s = []
i = 0
while i < 4: # or: while (i < 4):
s.append(i)
i += 2 # or: i = i + 2
t = []
i = 0
while True:
t.append(i)
i += 2
if i >= 4: break
# x, y, z, r, s, and t will contain the same values
Every one of the snippets above showcases a different
way of doing the exact same thing in Python. Some are
idiomatic, others ugly, some are outright bad code, yet all are perfectly valid Python.How would a novice programmer know which of the above is "good" code and what is the preferred way to do it?
A Python tutorial aimed at kids [1] never even mentions "range()", list-comprehensions, "+=", etc. yet enables children to write games in Python.
A Python intro for non-programmers [2] on the other hand shows both for- and while-loops as well as "range()".
So I'd argue that this is a non-starter as even the very basics as taught by recommended sites from the Python website itself [3] differ greatly in scope.
So I [edit]don't[/edit] find f-strings to be a big deal, especially since string formatting is a rather advanced topic anyway and beginners would stick with
for i in [1, 2, 3, 4, 5, 6, 7]:
print("The number", i, "is", "even" if i % 0 == 0 else "odd")
anyway instead of messing with f-string, %, or "format()" - whoops! - accidentally introduced yet another way of doing it :D[1] http://www.letslearnpython.com/learn/
[2] https://thepythonguru.com/python-loops/
[3] https://wiki.python.org/moin/BeginnersGuide/NonProgrammers
Yeah, and then - as the language matures - so do its users.
It's a common dilemma that cannot be fixed. You have the early adopters who grew up with the language and at some point got stuck in their ways (e.g. Python 2.x).
Replacing day-one features with new ones feels like using a new language to those who used the language for decades. The more mature (read: older and more widespread) a programming language becomes, the harder it is to introduce breaking changes.
String interpolation is one such area and due to the nature of the language such an important and integral part that it's becomes almost impossible to change.
As someone who never touched Python until very recently, I never even considered using anything but f-string formatting; I acknowledge that other (legacy) options exist, but outside of reading other people's code, I simply ignore them.
To new users who started with Python 3.x the obvious way to do it is f-strings and only language veterans switching to 3.x are affected by that feeling.
IMHO nothing changed. 2.x uses print without parentheses and the %-operator. 3.x requires parenthesis and prefers f-strings.
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 _`.
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?
_("Hello, {name}").format(name=name) _("Hello, {name}).format(**vars()) value = 5/6
precision = 2
print(f"{value:.{precision}f}")
> 0.83log.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.
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.)
(That's what I get for assuming consistent behavior :P)
import logging
got = "%d"
logging.warning(f"hi {got}")
logging.debug(f"hi {got}")
Output WARNING:root:hi %d
DEBUG:root:hi %dSo:
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()}))Also, shoutout to Template for untrusted strings. It’s surprisingly unknown. Most people suggest replace or a custom format function.
Limiting logging to `%`-formatting seems to be an inconsistency similar to supporting `%` but not `format` on `bytes` and `bytearray`.
the logging module predates the `.format` function.
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.
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.
Squiggly heredoc is something I am unaware of in any other general programming language.
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
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.
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?
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.
F-strings certainly seem like opt-in behavior to me.
There's a hack for that (no apologies for the self promotion; the project is a just funnin')
(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.)
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.
I'm not sure I expect it to handle every Unicode nuance either, when there are modules in the stdlib for that.
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?
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.They are customizable on an object-to-be-formatted basis (__format__ special method).
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.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"{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.)[0] https://github.com/psf/black/blob/master/docs/the_black_code...
$ echo "'foo'" | black -
"foo"
$ echo "'foo[\"bar\"]'" | black -
'foo["bar"]'
$ echo "'foo[\"bar\"]+\\'baz\\''" | black -
"foo[\"bar\"]+'baz'"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)
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. let name = "Bob";
print!("Hello {name}");
[0]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-i...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).”)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.
- 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?“=” in the format-spec (the thing that comes after the “:”) is the numeric padding alignment specifier (other alignment specifiers are left “<”, right “>”, and center “^”.)
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}")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/msg341732You 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.
`logger.debug("Got %s results", num)`
instead of
`logger.debug("Got {} results".format(num))`
is that in the first case, internally it checks if the logger is disabled and if so skips formatting the string, whereas in the second case you've already formatted it before passing it in, so that work can't be avoided. This isn't the case for print statements because they don't execute conditionally, so you don't ever skip doing the format.
In Python 2 I used to use .format(**locals()) which is basically the same thing, but felt like a hack.
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.
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!
https://www.jetbrains.com/help/pycharm/configuring-third-par...
That said most apps probably won’t see much of a difference in perf between the two approaches.
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.
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.
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.
While I know why building f strings is forbidden (they can run arbitrary code), I'm happy to take the risk.
>>> fstring="Hello, {\"world\"}"
>>> eval(f"print(f'{fstring}')")
Hello, worldhttps://pyfound.blogspot.com/2020/04/all-strings-become-f-st...
It was a beautiful thing and made it possible to write the same "hello world" program in Python and BASIC:
print "hello, world"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.)
Shameless plug, last year I wrote one myself to document everything I knew about it.
https://miguendes.me/73-examples-to-help-you-master-pythons-...
>>> blah = "something \here"
>>> fr"what the {blah}"
'what the something \\here'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 \\aThey 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.
f-strings were released in Python 3.6 in December 2016. This isn't news.
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 ...