Python / Strings

f-strings vs .format() vs % formatting

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

CodeTypeExampleOutput
%sstring (calls str())"%s" % 4242
%dinteger"%d" % 3.93 (truncated)
%ffloat"%.2f" % 3.13.10
%rrepr()"%r" % "hi"'hi'
%xhex integer"%x" % 255ff

Pitfalls

The single-value case requires a tuple to avoid a TypeError when the value itself is a tuple:

Breaks with tuples
t = (1, 2)
print("%s" % t)
# TypeError: not all args converted
Safe form
t = (1, 2)
print("%s" % (t,))   # wrap it
# (1, 2)
Note The Python docs describe % formatting as a feature that may be removed in a future version. It remains common in older codebases and logging (where it's evaluated lazily), but write no new code with it.

.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))
Note Never call .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))
Exception: logging The 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.