Hacker News new | past | comments | ask | show | jobs | submit login

f-strings are the first truly-pretty way to do string formatting in python, and the best thing is that they avoid all of the shortcomings of other interpolation syntaxes I've worked with. It's one of those magical features that just lets you do exactly what you want without putting any thought at all into it.

Digression on the old way's shortcomings: Probably the most annoying thing about the old "format" syntax was for writing error messages with parameters dynamically formatted in. I've written ugly string literals for verbose, helpful error messages with the old syntax, and it was truly awful. The long length of calls to "format" is what screws up your indentation, which then screws up the literals (or forces you to spread them over 3x as many lines as you would otherwise). It was so bad that the format operator was more readable. If `str.dedent` was a thing it would be less annoying thanks to multi-line strings, but even that is just a kludge. A big part of the issue is whitespace/string concatenation, which, I know, can be fixed with an autoformatter [0]. Autoformatters are great for munging literals (and diff reduction/style enforcement), sure, but if you have to mung literals tens of times in a reasonably-written module, there's something very wrong with the feature that's forcing that behavior. So, again: f-strings have saved me a ton of tedium.

[0] https://github.com/python/black

For me, the hugs thing about f-strings was that invalid string format characters become a compile time error (SyntaxError).

  print('I do not get executed :)')

  File "stefco.py", line 2
  SyntaxError: f-string: empty expression not allowed
This has the pleasing characteristic of eliminating an entire class of bug. :)

> If `str.dedent` was a thing

Have you looked at textwrap.dedent?

Yes! `textwrap.dedent` is great. On further reflection `wrap` is actually more useful for this kludge (see below). But my point is that that's a whole import for a kludge. Compare the f-string ideal (by my standards):

  raise ValueError("File exists, not uploading: "
                   f"{filename} -> {bucket}, {key}")
...which is short enough that it's readable, and it's clear where exactly each variable is going. It's the single obvious solution, so much so that I don't spend a second thinking about it (very Pythonic!). Compare it to using `str.format` with the same continued indentation:

  raise ValueError(("File exists, not uploading: {filename} -> "
                    "{bucket}, {key}").format(filename=filename,
Even this minimal example looks terrible! Remember that a lot of exceptions are raised within multiply-nested blocks, and then format pushes things farther to the right (while also ruining your automated string-literal concatenation, hence the extra parentheses), leaving very little room for the format arguments. You can use a more self-consistent and readable indentation strategy:

  raise ValueError(
          "File exists, not uploading: {filename} -> "
          "{bucket}, {key}"
      ).format(filename, bucket, key)
This is unquestionably more pleasant to read than the former, but it's 3 times longer than the simple f-string solution, and I would argue it is not any more readable than the f-string for this simple example. My point with having a `str.wrap` builtin is that at least you could use the docstring convention of terminating multi-line strings on a newline, which would get rid of the string concatenation issues while leaving you a consistent (albeit diminished by the "wrap" call) amount of rightward room for the `format` args:

  raise ValueError("""File exists, not uploading: {filename} ->
                   {bucket}, {key}
                                       bucket=bucket, key=key))
Maybe a little bit better than the first one, especially if you're writing a longer docstring and don't want to think about string concatenation. But still a kludge. You can use positional formatting to shorten things up, but the fundamental weakness of `str.format` remains.

Here's a clean way to do that:

  str_fmt = "File exists, not uploading: {filename} -> {bucket}, {key}"
  fmt_vals = dict(filename=filename, bucket=bucket, key=key)
  raise ValueError(str_fmt.dedent().format(**fmt_vals))

This is somewhat cleaner, and I also use this idiom when things get ugly with the inline formatting shown above. But my point is that none of these are very elegant for an extremely common use case. Throw this block in the middle of some complex code with a few try/except and raise statements and it still looks confusing. Having two extra temp variables and statements per error in a function that's just doing control flow and wrapping unsafe code can double your local variable count and number of statements across the whole function. AFAIK, there has been no elegant solution to this common problem until f-strings came around; the only decently clean one is using printf-style format strings with the old-style operator, but outside of terseness I find it less readable.

Alternate “clean” way, but sort of hacky.

  raise ValueError(“File exists, not uploading: {filename} -> {bucket}, {key}”.format(**locals()))

f-strings are nice, but when the problem is indenting too far, what if you just... didn't do that?

  raise ValueError(("File exists, not uploading: {filename} -> "
      "{bucket}, {key}").format(filename=filename, bucket=bucket, key=key))

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact