Python String Formatting: f-strings, format(), and When Each One Wins
Python has three mature string formatting systems and one more niche one. They’ve accumulated over the language’s history, and each still has valid use cases. The trick is knowing which to reach for when — and knowing the format specification mini-language that makes all of them more powerful.
f-strings (Python 3.6+)
F-strings are the modern standard. Prefix a string with f or F, and anything inside curly braces is evaluated as a Python expression:
name = "Alice"age = 30score = 94.333
print(f"Name: {name}")print(f"Age next year: {age + 1}")print(f"Score: {score:.2f}") # 94.33 — two decimal placesprint(f"Name: {name!r}") # 'Alice' — repr(), shows quotesprint(f"Name: {name!u}") # ALICE — .upper() shorthand? No — use expressions:print(f"Name: {name.upper()}") # ALICE — expressions workf-string debugging (Python 3.8+)
Add = after the expression to print the expression and its value — invaluable for debugging:
x = 42items = [1, 2, 3]print(f"{x = }") # x = 42print(f"{len(items) = }") # len(items) = 3Format spec mini-language
The colon inside f-string braces introduces a format specification:
n = 1234567.89
# Number formattingprint(f"{n:,.2f}") # 1,234,567.89 — comma separator, 2 decimal placesprint(f"{n:.0f}") # 1234568 — rounded, no decimalprint(f"{n:e}") # 1.234568e+06 — scientific notation
# Alignment and paddinglabel = "Total"value = 99.5print(f"{label:>10}: {value:<10.1f}") # right-align label, left-align value
# Column formattingfor name, score in [("Alice", 92), ("Bob", 78), ("Charlie", 100)]: print(f"{name:<12}{score:>6}")
# Integer formattingn = 255print(f"{n:d}") # 255 — decimalprint(f"{n:b}") # 11111111 — binaryprint(f"{n:o}") # 377 — octalprint(f"{n:x}") # ff — hexadecimalprint(f"{n:#x}") # 0xff — hex with prefixprint(f"{n:08b}") # 11111111 — zero-padded to 8 digitsWhen to use f-strings
Use f-strings by default for any formatting in Python 3.6+. They’re the most readable, fastest at runtime, and support the full format spec.
str.format()
str.format() was the modern approach before f-strings arrived. It supports the same format spec mini-language but embeds values differently:
# Positional argumentsprint("Hello, {}!".format("Alice"))
# Indexed argumentsprint("{0} and {1}, or {1} and {0}".format("Python", "JavaScript"))
# Named argumentsprint("Name: {name}, Score: {score:.1f}".format(name="Alice", score=94.33))
# From a dictionarydata = {"city": "Paris", "country": "France"}print("{city} is in {country}".format(**data))When to use str.format()
- When you need the format string to be a variable (template stored in a database, config file, or passed in):
templates = { "welcome": "Welcome back, {name}! You have {count} messages.", "farewell": "Goodbye, {name}. See you soon.",}
def render(template_key, **kwargs): return templates[template_key].format(**kwargs)
print(render("welcome", name="Alice", count=3))You cannot use f-strings for runtime-defined templates because f-strings are evaluated when the source code runs, not when a variable is used. str.format() evaluates at call time.
- When targeting Python 3.5 or earlier (though this is rare now).
% Formatting
The oldest method, borrowed from C’s printf. Still found in older codebases and some logging configurations:
name = "Alice"score = 94.33
print("Name: %s, Score: %.1f" % (name, score))print("Items in stock: %d" % 42)print("Hex value: %x" % 255) # ffFormat specifiers: %s (string), %d (integer), %f (float), %x (hex), %.2f (float with 2 decimal places).
When to use % formatting
- In logging calls. Python’s
loggingmodule uses%-style formatting and delays substitution until the message is actually needed:
import logginglogging.basicConfig(level=logging.DEBUG)
username = "alice"logging.debug("User %s logged in", username) # efficient — no string built if not loggedUsing f-strings with logging defeats the performance optimisation:
logging.debug(f"User {username} logged in") # string always built, even if debug is offTemplate Strings
string.Template is the fourth option, designed specifically for user-supplied format strings where f-strings or str.format() would be a security risk:
from string import Template
# $ prefix instead of {}template = Template("Hello, $name! You have $count messages.")result = template.substitute(name="Alice", count=5)print(result) # Hello, Alice! You have 5 messages.
# safe_substitute() doesn't fail on missing keyspartial = Template("Dear $name, your order #$id has shipped.")print(partial.safe_substitute(name="Bob"))# Dear Bob, your order #$id has shipped.When to use Template strings
When the format string comes from untrusted input. If a user can supply the format string and you use str.format(), they can access arbitrary attributes of your objects:
# Security problem with format()class Config: password = "secret123"
config = Config()user_template = "{config.__class__.__init__.__globals__}" # malicious input# This can expose internal data
# Template strings don't have this problem — $ substitution is restrictedFor admin tools, email templates loaded from a database, or any situation where users control the template string, use string.Template.
Quick Reference
| Method | When to use |
|---|---|
| f-string | Default for everything in Python 3.6+ |
| str.format() | When template is a runtime variable |
| % operator | In logging calls; when maintaining old code |
| string.Template | When users supply the template string |
The format spec mini-language (:,.2f, :<10, :>8, :b, etc.) works the same way in f-strings and str.format(). Learn it once and apply it to both.