Python has three ways to embed values in strings. All three produce the same output in basic cases. They differ in syntax, readability, performance, and flexibility.
| Method | Syntax | Since | Readability | Speed | Use today? |
|---|---|---|---|---|---|
| f-strings | f"..." |
3.6 | High | Fastest | Preferred |
| .format() | "...".format() |
2.6 | Medium | Slower | Template use |
| % formatting | "..." % (...) |
2.0 | Low | Medium | Avoid (legacy) |
% Formatting (Old-style)
Inherited from C's printf. Uses format codes like %s (string), %d (integer), %f (float). You pass values via a tuple on the right side of %.
name = "ada" age = 36 print("%s is %d years old" % (name, age)) # ada is 36 years old print("pi ≈ %.4f" % 3.14159265) # pi ≈ 3.1416 print("%-10s|" % "left") # left-pad to 10 chars # left |
Common format codes
| Code | Type | Example | Output |
|---|---|---|---|
%s | string (calls str()) | "%s" % 42 | 42 |
%d | integer | "%d" % 3.9 | 3 (truncated) |
%f | float | "%.2f" % 3.1 | 3.10 |
%r | repr() | "%r" % "hi" | 'hi' |
%x | hex integer | "%x" % 255 | ff |
Pitfalls
The single-value case requires a tuple to avoid a TypeError when the value itself is a tuple:
t = (1, 2) print("%s" % t) # TypeError: not all args converted
t = (1, 2) print("%s" % (t,)) # wrap it # (1, 2)
.format() Method
Introduced in Python 2.6. Uses {} placeholders. Values can be referenced by position, by name, or left implicit. More flexible than % but verbose for inline use.
# Positional "{} is {} years old".format("ada", 36) # ada is 36 years old # Indexed — reuse or reorder values "{0} + {0} = {1}".format(5, 10) # 5 + 5 = 10 # Named "{name} runs on {os}".format(name="bash", os="Linux") # bash runs on Linux # Format spec — same mini-language as f-strings "{:.4f}".format(3.14159) # 3.1416 "{:>10}".format("right") # right
Storing templates
The primary reason to still use .format() is that templates can be stored as plain strings and filled in later — without executing arbitrary code:
# Template defined once, filled at runtime TEMPLATE = "Dear {name},\nYour order #{order_id} has shipped." def render(name, oid): return TEMPLATE.format(name=name, order_id=oid) print(render("alice", 9021))
.format(**user_input) on a template supplied by users. It can leak object attributes via {obj.__class__.__dict__}-style access. Use string.Template for untrusted input instead.
f-strings (Formatted String Literals)
Available from Python 3.6. Any expression inside {} is evaluated at runtime. The string is prefixed with f or F.
name = "ada" age = 36 print(f"{name} is {age} years old") # ada is 36 years old # Inline expressions print(f"{2 ** 10}") # 1024 print(f"{name.upper()}") # ADA print(f"{age * 2}") # 72 # Format spec (same mini-language as .format()) print(f"{3.14159:.3f}") # 3.142 print(f"{1_000_000:,}") # 1,000,000 print(f"{255:#010b}") # 0b11111111 # Conditional inline score = 73 print(f"{'pass' if score >= 50 else 'fail'}") # pass
Self-documenting expressions (3.8+)
Add = after the expression to print both the expression and its value. Useful for debugging.
x = 42 print(f"{x=}") # x=42 print(f"{x * 2 + 1=}") # x * 2 + 1=85
Multiline f-strings
user = {"name": "alice", "uid": 1001} msg = ( f"Name : {user['name']}\n" f"UID : {user['uid']}\n" f"Shell: /bin/bash" ) print(msg)
What you cannot do in an f-string
# Backslashes are not allowed inside the braces (before 3.12) f"{'\n'.join(items)}" # SyntaxError on 3.11 and below # Workaround: assign first sep = '\n' f"{sep.join(items)}" # OK # Python 3.12 removed this restriction entirely
Format Specification Mini-Language
Both f-strings and .format() use the same spec syntax after the colon: {value:spec}.
# [[fill]align][sign][#][0][width][grouping][.precision][type] f"{42:08d}" # 00000042 — zero-pad integer to width 8 f"{3.14:.2f}" # 3.14 — 2 decimal places f"{1234567:_d}" # 1_234_567 — underscore thousands sep f"{0.5:.0%}" # 50% — percentage f"{255:x}" # ff — hexadecimal f"{255:X}" # FF — uppercase hex f"{255:#010x}" # 0x000000ff — hex with 0x prefix, zero-padded f"{255:08b}" # 11111111 — binary f"{'left':<10}" # left — left-align in 10 chars f"{'right':>10}" # right — right-align f"{'mid':^10}" # mid — center-align f"{'mid':*^10}" # ***mid**** — center with fill char
Performance
For most scripts, the difference is irrelevant. In tight loops over millions of iterations, f-strings are measurably faster because they compile to optimized bytecode that avoids function-call overhead.
# Rough benchmarks — 1 million iterations, CPython 3.12 # f-string : ~0.08 s # .format() : ~0.22 s (~2.7× slower) # % format : ~0.16 s (~2.0× slower) # Benchmark it yourself: import timeit print(timeit.timeit(lambda: f"{'x'} {'y'}", number=1_000_000)) print(timeit.timeit(lambda: "{} {}".format("x", "y"), number=1_000_000)) print(timeit.timeit(lambda: "%s %s" % ("x", "y"), number=1_000_000))
logging module uses % formatting intentionally. Pass the format string and args separately — logging.debug("value: %s", x) — so the string is only interpolated if the message will actually be emitted. Using f-strings here always evaluates the expression, even when logging is disabled at that level.
Decision Guide
Use f-strings when
You are writing Python 3.6+ code (which is all current code). This is the right default for nearly everything: one-off messages, debug output, building strings from variables, formatting numbers.
Use .format() when
You need to store a reusable template as a string constant and fill it in at different points in the program. Also useful when the template comes from a config file or database (but validate carefully — see the security note above).
# Config-driven template — .format() makes sense here LOG_FMT = "[{level}] {ts} — {msg}" def log(level, ts, msg): return LOG_FMT.format(level=level, ts=ts, msg=msg)
Use % formatting when
You are maintaining existing code that uses it. Do not convert working %-formatted strings unless you are refactoring the file anyway. Also keep it in logging calls as described above.