PYTHON / STRINGS
format() and percent formatting
Fill templates stored in variables using str.format() and the % operator, with positional, keyword, dict, width and precision specs.
What you will learn
- Apply a template held in a variable with .format() or %, which an f-string cannot do
- Reuse one argument via {0} and feed dicts with .format(**d) or template % d
- Escape a literal brace as {{ }} and a literal percent sign as %%
- Pass a tuple as a single %s value by writing template % (value,)
Understanding format() and percent formatting
str.format() and the % operator both separate the template from the values: the placeholder string is ordinary data that you can store in a variable, load from a config file, or reuse in a loop, and the substitution happens when you call .format() or apply %. In a format string, each replacement field is written in braces and may name its argument by position ({}, {0}) or by keyword ({name}); anything after a colon is a format spec such as .2f, >8 or , which controls conversion, width and alignment.
The % operator is the older printf-style mechanism: the left operand is the template, the right operand is one value, a tuple of values, or a mapping. The conversion character matters — %s calls str(), %r calls repr(), and %d insists on a number, so "%d" % "7" raises TypeError instead of quietly converting. Because a single tuple on the right is interpreted as the whole argument list, printing a tuple needs the awkward point % (point,) form; this ambiguity is the main reason % formatting is error-prone.
The format spec after the colon in .format() is the exact same mini-language f-strings use, so learning {:>10,.2f} once covers both tools. Reach for .format() when the template arrives at runtime rather than being typed in your source — translation catalogues, report layouts chosen by the user, rows of the same shape printed repeatedly. Keep % formatting for the logging module, where logger.info("saved %s rows", n) leaves the formatting work undone unless the message is actually emitted.
template = "{name} scored {score:.1f} out of {total}"
print(template.format(name="Ada", score=91.456, total=100))
print(template.format(name="Bo", score=78.0, total=100))
# positional indices let one argument appear twice
print("{0}-{1}-{0}".format("a", "b"))
# percent style: the values arrive as a tuple
print("%s used %d%% of the quota" % ("Ada", 91))
# width and alignment exist in both styles
print("|{:>8}|{:<8}|".format("right", "left"))
print("|%8s|%-8s|" % ("right", "left"))format() and % apply a template to values supplied later, so the template can itself be a variable, while an f-string is fixed where it is written.
Worked examples
Filling a template from a dict
Both styles can take their values from a mapping instead of separate arguments.
row = {"city": "Oslo", "pop": 709037}
print("{city} has {pop:,} people".format(**row))
print("%(city)s has %(pop)d people" % row)
# the layout itself is data, so it can be swapped
for layout in ["{0:>6}: {1}", "{1} ({0})"]:
print(layout.format("pop", row["pop"]))Example explained
Line 1**row turns dict keys into keyword arguments, so {city} and {pop} resolve by name.
Line 2The % operator accepts a mapping directly with the %(key)s spelling — no unpacking needed.
Line 3{pop:,} inserts thousands separators; %d has no equivalent flag, hence the bare 709037.
Line 4Each layout string is just a value in a list, which is precisely what an f-string cannot be.
Escaping braces and percent signs
Literal { } and % must be doubled, and a lone tuple on the right of % is misread as the argument list.
print("{{{}}}".format("x"))
print("%d%% done" % 50)
point = (3, 4)
print("point=%s" % (point,))
try:
print("point=%s" % point)
except TypeError as e:
print("TypeError:", e)Example explained
Line 1"{{{}}}" is read as {{ then {} then }}, producing a brace, the value, and a closing brace.
Line 2%% is the only way to get a literal percent sign in a %-formatted template.
Line 3% (point,) wraps the tuple in a one-element tuple so it counts as a single %s value.
Line 4Without the wrapper, Python sees two values for one placeholder and refuses to format.
Automatic and manual field numbering do not mix
Once a format string omits indices, adding an explicit index is a hard error.
row = ("Ada", 91)
print("{} scored {}".format(*row))
print("{0} scored {0}".format(*row))
try:
print("{} scored {0}".format(*row))
except ValueError as e:
print("ValueError:", e)Example explained
Line 1Empty braces consume arguments left to right, so {} {} maps to row[0] then row[1].
Line 2{0} twice is legal and reuses the first argument, which empty braces can never do.
Line 3Mixing the two styles is rejected because the counter for automatic fields would be ambiguous.
Line 4Fix it by indexing every field: "{0} scored {1}".
Important notes
The part after the colon in .format() is the identical format spec f-strings use, so {:>10,.2f} behaves the same in both; the %-style flags are a separate, smaller syntax.
Do not pre-format logging messages. logger.info("saved %s rows", n) lets logging skip the formatting entirely when the level is disabled.
Common mistakes
Passing a tuple to a single %s without wrapping it: "%s" % (3, 4) raises TypeError: not all arguments converted during string formatting instead of printing the pair.
Mixing {} and {0} in one format string, which raises ValueError: cannot switch from automatic field numbering to manual field specification at call time, not at import time.
Leaving a stray % in a %-formatted template ("50% done" % ()): the % plus the next letter is parsed as a conversion, so you get TypeError: not enough arguments for format string.
Try it yourself
Change, predict, then run
Store the template "{label:<10}|{value:>8.2f}" in a variable and print a row for each pair in [("rent", 1200.0), ("coffee", 3.5)], then produce byte-identical output using a %-style template.
Open the Python workspaceCheck your understanding
A template arrives at runtime: tmpl = cfg["greeting"], whose value is the text '{name}, you have {count} messages'. How do you fill it in?
- tmpl.format(name=user, count=n)
- f"{tmpl}"
- f"{tmpl}".format(user, n)
- tmpl % (user, n)
Show answer
An f-string is compiled where the literal is written, so a template loaded at runtime can never be one; f"{tmpl}" just interpolates the variable and returns the braces verbatim. tmpl % (user, n) fails too, because the template contains {name}-style fields and no % conversions, so Python reports that not all arguments were converted.