String Formatting in Python: A Working Guide to f-strings and Their Alternatives
Python has three main string formatting mechanisms: the % operator (legacy), str.format() (Python 2.6+), and f-strings (Python 3.6+). All three appear in real codebases. Knowing all of them helps you read older code and make informed choices for new code.
f-strings โ The Modern Default
f-strings (formatted string literals) evaluate expressions directly inside {}. They are the fastest option and the most readable for most cases.
name = "Alice"age = 30balance = 12345.678
# Basic variable insertionprint(f"Name: {name}, Age: {age}")# Name: Alice, Age: 30
# Any expression works inside {}print(f"Next year: {age + 1}")print(f"Upper: {name.upper()}")print(f"Length: {len(name)}")
# Number formatting with format specprint(f"Balance: ${balance:,.2f}") # $12,345.68 โ comma + 2 decimal placesprint(f"Percent: {0.876:.1%}") # 87.6%print(f"Padded: {name:>10}") # ' Alice' โ right-align in 10 charsprint(f"Padded: {name:<10}|") # 'Alice |' โ left-alignprint(f"Padded: {name:^10}|") # ' Alice |' โ centerFormat Spec Mini-Language
The format spec after : is the same for f-strings and str.format():
value = 3.14159265
# Decimal placesprint(f"{value:.2f}") # 3.14print(f"{value:.5f}") # 3.14159
# Scientific notationprint(f"{value:.2e}") # 3.14e+00
# Integer formattingn = 255print(f"{n:d}") # 255 โ decimalprint(f"{n:b}") # 11111111 โ binaryprint(f"{n:o}") # 377 โ octalprint(f"{n:x}") # ff โ hex lowercaseprint(f"{n:X}") # FF โ hex uppercaseprint(f"{n:08b}") # 11111111 โ zero-padded to 8 chars
# Alignment and fillprint(f"{'left':<10}|") # 'left |'print(f"{'right':>10}|") # ' right|'print(f"{'center':^10}|") # ' center |'print(f"{'fill':*^10}|") # '**fill****|' โ fill with *str.format() โ Positional and Keyword Arguments
str.format() is more verbose but more portable (works in Python 2.6+ if you ever encounter it):
# Positionalprint("Hello, {}! You are {} years old.".format("Bob", 25))# Hello, Bob! You are 25 years old.
# Namedprint("Name: {name}, Role: {role}".format(name="Carol", role="admin"))
# Reuse the same argumentprint("{0} + {0} = {1}".format(5, 10))# 5 + 5 = 10
# Format spec works the same wayprice = 49.99print("Price: ${:.2f}".format(price)) # Price: $49.99
# From a dictdata = {"city": "London", "temp": 18.5}print("Weather in {city}: {temp:.1f}ยฐC".format(**data))# Weather in London: 18.5ยฐC% Formatting โ Legacy Style
You will see this in older code and in logging calls (where it has a specific performance advantage โ the string is only formatted if the log level is active).
name = "Dave"age = 40score = 98.765
# %s = string, %d = integer, %f = floatprint("Name: %s, Age: %d" % (name, age))print("Score: %.2f%%" % score) # doubled % to escape it# Score: 98.77%
# Named placeholdersprint("%(name)s is %(age)d years old." % {"name": name, "age": age})
# Logging โ % formatting deferred until the message is actually emittedimport logginglogging.basicConfig(level=logging.DEBUG)logging.debug("User %s logged in from %s", name, "192.168.1.1")The logging module intentionally uses % formatting so that string interpolation is skipped when the log message will not be emitted (e.g. debug messages in production).
Multi-line f-strings
name = "Alice"items = ["widget", "gadget", "thingamajig"]total = 157.50
receipt = ( f"Customer: {name}\n" f"Items: {len(items)}\n" f"Total: ${total:.2f}\n" f"Thank you for your purchase!")print(receipt)
# Triple-quoted f-stringreport = f"""Name: {name}Items: {', '.join(items)}Total: ${total:.2f}"""print(report.strip())Debugging with f-string = (Python 3.8+)
The = specifier prints both the expression and its value:
x = 42y = [1, 2, 3]print(f"{x=}") # x=42print(f"{y=}") # y=[1, 2, 3]print(f"{len(y)=}") # len(y)=3print(f"{x * 2 + 1=}") # x * 2 + 1=85This is much faster to type than print(f"x = {x}") and ensures the label always matches the variable name.
Choosing the Right Method
| Method | Python version | Readability | Speed | Best for |
|---|---|---|---|---|
| f-string | 3.6+ | Highest | Fastest | New code, most situations |
| str.format() | 2.6+ | Medium | Medium | Template strings, compatibility |
| % formatting | All | Low | Slowest* | Legacy code, logging calls |
*For logging, % is actually preferred because the formatting is lazy โ the string is only built if the log is actually emitted.
Use f-strings by default. Use str.format() when you need to separate the template from the values (e.g. loading format strings from config). Leave % formatting in old code unless you are refactoring the whole module.