String Formatting: f-strings, format() and %

Three formatting styles exist. f-strings are the one to use, and the format specification they share controls width, alignment, precision and thousands separators.

The three styles

name = "Meera"
score = 92.5

print("Hello %s, you scored %.1f" % (name, score))          # oldest
print("Hello {}, you scored {:.1f}".format(name, score))    # older
print(f"Hello {name}, you scored {score:.1f}")              # current

All three produce the same output. Use f-strings, available since Python 3.6. Know the other two because you will meet them in existing code, and because % style is still what the logging module expects.

f-strings

An f-string is a normal string literal prefixed with f. Anything inside braces is an expression, evaluated at that point and inserted.

name = "Meera"
age = 27
items = ["pen", "book"]

print(f"{name} is {age}")
print(f"Next year: {age + 1}")                 # any expression
print(f"Upper: {name.upper()}")                # method calls
print(f"First item: {items[0]}")               # indexing
print(f"Count: {len(items)}")                  # function calls
print(f"{'yes' if age >= 18 else 'no'}")     # a conditional expression

The equals sign for debugging

total = 42
rate = 0.18

print(f"{total=}")             # total=42
print(f"{rate=:.2f}")          # rate=0.18
print(f"{total * rate=}")      # total * rate=7.56

Since Python 3.8, appending = prints the expression text as well as the value. It replaces a great many throwaway print("total is", total) lines.

Braces and quotes

print(f"{{literal braces}}")           # {literal braces}

data = {"name": "Meera"}
print(f"{data['name']}")                 # different quote style inside
# From Python 3.12 the same quote style is allowed inside as well.

The format specification

After a colon inside the braces comes the format specification. It is shared by f-strings and format(), so learning it once covers both.

{value:[[fill]align][sign][#][0][width][grouping][.precision][type]}

Width and alignment

word = "cat"

print(f"[{word:10}]")        # [cat       ]  text defaults to left
print(f"[{word:<10}]")       # [cat       ]
print(f"[{word:>10}]")       # [       cat]
print(f"[{word:^10}]")       # [   cat    ]
print(f"[{word:*^10}]")      # [***cat****]

number = 42
print(f"[{number:10}]")      # [        42]  numbers default to right

Numbers

value = 1234567.8915

print(f"{value:.2f}")         # 1234567.89     two decimal places
print(f"{value:,.2f}")        # 1,234,567.89   thousands separators
print(f"{value:_.2f}")        # 1_234_567.89
print(f"{value:15,.2f}")      # width 15, right aligned, grouped
print(f"{value:e}")           # 1.234568e+06

print(f"{0.256:.1%}")         # 25.6%          percentage
print(f"{42:05d}")            # 00042          zero padded
print(f"{42:+d}")             # +42            always show the sign
print(f"{-42:+d}")            # -42

Other bases

n = 255
print(f"{n:b}", f"{n:o}", f"{n:x}", f"{n:X}")     # 11111111 377 ff FF
print(f"{n:#x}", f"{n:#b}")                        # 0xff 0b11111111
print(f"{n:08b}")                                  # 11111111 padded to 8 bits

Dynamic width

width = 12
for name in ["Meera", "Arun"]:
    print(f"{name:>{width}}")      # the width itself comes from a variable

A formatted report

rows = [
    ("Notebook", 3, 45.50),
    ("Pen", 12, 8.75),
    ("Highlighter set", 2, 199.00),
]

print(f"{'Item':<20}{'Qty':>5}{'Price':>12}{'Total':>12}")
print("-" * 49)

grand_total = 0
for item, quantity, price in rows:
    line_total = quantity * price
    grand_total += line_total
    print(f"{item:<20}{quantity:>5}{price:>12,.2f}{line_total:>12,.2f}")

print("-" * 49)
print(f"{'Grand total':<37}{grand_total:>12,.2f}")
Item                  Qty       Price       Total
-------------------------------------------------
Notebook                3       45.50      136.50
Pen                    12        8.75      105.00
Highlighter set         2      199.00      398.00
-------------------------------------------------
Grand total                                 639.50

str.format

print("{} scored {}".format("Meera", 92))
print("{0} scored {1}, and {0} passed".format("Meera", 92))    # by position
print("{name} scored {score}".format(name="Meera", score=92))  # by name

record = {"name": "Meera", "score": 92}
print("{name} scored {score}".format(**record))

format remains useful for a template stored in a variable, where an f-string cannot be used because there is nothing to interpolate at definition time.

Percent formatting

print("%s scored %d out of %d" % ("Meera", 92, 100))
print("%.2f%%" % 25.6)          # 25.60%   - %% is a literal percent sign

import logging
logging.warning("User %s failed %d times", "meera", 3)   # keep this style here

The logging module defers formatting until it knows the message will actually be emitted, so passing arguments separately, in % style, is measurably better there.

Choosing between them

SituationUse
Normal codef-string
Template stored in a variable or filestr.format
Logging calls% style with separate arguments
Building SQL or shell commandsNone of them - use the tool's parameter binding
Never build SQL by formatting user input into a query string. That is SQL injection. Use parameter placeholders, which the security notes cover in detail.

Common mistakes

  • Forgetting the f prefix, so the braces print literally.
  • Writing {value:2f} instead of {value:.2f}. Without the dot it is a width, not a precision.
  • Assuming {:.2f} rounds for storage. It formats for display only.
  • Using the same quote character inside an f-string on Python versions before 3.12.
  • Mixing % and format styles in one project.
  • Formatting user data into a query or a command.

Best practices

  • Use f-strings everywhere except logging and stored templates.
  • Use {x=} for debug output; it is shorter and it never mislabels a value.
  • Use , grouping for any number a person will read.
  • Keep expressions inside f-strings short. Compute first, format second.

Practice

  1. Print 1234567.891 as 1,234,567.89, as 1.23e+06 and as a right aligned field of width 20.
  2. Print a three column table of five records with aligned headings and a total row.
  3. Show 0.0725 as 7.25%.
  4. Explain why f"{name}" and "{}".format(name) both work, and give a case where only the second is possible.
  5. Print the numbers 1 to 16 in binary, each padded to four digits, four per line.

Conclusion

Use f-strings. Learn the specification after the colon once - fill, align, width, grouping, precision, type - and it works identically in format(). Keep % style for logging, and never format untrusted input into a query.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.