Overview
Python has four string formatting mechanisms. F-strings (3.6+) are the default choice for new code: they are readable, fast, and evaluated at the call site. Use str.format when the template lives in a variable or config file; use format_map when the values come from a dict and missing keys should raise clearly. Avoid percent-style (%) in new code; it survives in logging calls for lazy evaluation. This card covers syntax, specifiers, and the non-obvious edges.
F-string syntax
F-strings are string literals prefixed with f or F. Expressions inside {} are evaluated at runtime.
| Pattern | Example | Output |
|---|---|---|
| Simple variable | f"{name}" | "Alice" |
| Expression | f"{2 + 2}" | "4" |
| Method call | f"{name.upper()}" | "ALICE" |
| Attribute | f"{obj.value}" | attribute value |
| Dict access | f"{d['key']}" | dict value (use different quote inside) |
| Conditional | f"{'yes' if ok else 'no'}" | conditional value |
| Self-documenting (3.8+) | f"{value=}" | "value=42" |
| Nested f-string | f"{f'{x:.{prec}f}'}" | dynamic precision |
| Multiline | f"line1\n{var}" | newline is literal \n not backslash |
Backslash escapes are not allowed inside {} in Python < 3.12. Use a variable for complex expressions to stay readable.
str.format and format_map
Use str.format when the template is a runtime string. Use format_map to pass a mapping and allow custom __missing__ behavior.
| Pattern | Example | Notes |
|---|---|---|
| Positional | "{0} {1}".format(a, b) | Index order explicit |
| Keyword | "{x} {y}".format(x=1, y=2) | Readable, order-independent |
| Reuse positional | "{0} {0}".format(a) | Same arg twice |
| Dict unpack | "{x}".format(**d) | Unpack a dict |
format_map | "{x}".format_map(d) | Avoids copy; supports __missing__ |
| Attribute access | "{0.name}".format(obj) | Dot access inside template |
| Index access | "{0[key]}".format(d) | Bracket access inside template |
format_map with a defaultdict or custom class lets you silently skip missing keys, useful for partial template rendering.
Format specifiers
The format spec goes after : inside {}. The grammar is [[fill]align][sign][z][#][0][width][grouping][.precision][type].
| Specifier | Meaning | Example | Output |
|---|---|---|---|
d | Integer decimal | f"{42:d}" | "42" |
f | Float fixed-point | f"{3.14159:.2f}" | "3.14" |
e | Scientific notation | f"{12345.6:.2e}" | "1.23e+04" |
g | General (auto e/f) | f"{0.00012:.2g}" | "0.00012" |
% | Percentage | f"{0.42:.1%}" | "42.0%" |
s | String | f"{'hi':>10s}" | " hi" |
b | Binary | f"{10:b}" | "1010" |
x / X | Hex lower/upper | f"{255:x}" | "ff" |
o | Octal | f"{8:o}" | "10" |
n | Locale-aware number | f"{1000000:n}" | locale-dependent |
, | Thousands separator | f"{1000000:,}" | "1,000,000" |
_ | Underscore separator | f"{1000000:_}" | "1_000_000" |
08d | Zero-pad width 8 | f"{42:08d}" | "00000042" |
>10 | Right-align width 10 | f"{'hi':>10}" | " hi" |
<10 | Left-align width 10 | f"{'hi':<10}" | "hi " |
^10 | Center width 10 | f"{'hi':^10}" | " hi " |
+d | Force sign | f"{42:+d}" | "+42" |
#x | Hex with prefix | f"{255:#x}" | "0xff" |
Dynamic width and precision: f"{value:{width}.{prec}f}" where width and prec are variables.
Percent-style formatting
Percent-style predates str.format. Retain it only in logging calls where lazy evaluation avoids formatting strings that are never emitted.
| Pattern | Example | Notes |
|---|---|---|
%s | "Hello %s" % name | Calls str() |
%d | "Value: %d" % n | Integer |
%f | "%.2f" % 3.14 | Float fixed |
%r | "%r" % obj | Calls repr() |
| Named | "%(key)s" % d | Dict lookup |
| Logging | log.info("x=%s", val) | Lazy; do not use f"x={val}" here |
Never use percent-style for user-facing output in new Python 3 code. Use f-strings or str.format.
Datetime and custom __format__
format() calls __format__ on any object. Datetime objects accept strftime-style codes directly.
| Pattern | Example | Output |
|---|---|---|
| Date | f"{dt:%Y-%m-%d}" | "2026-05-14" |
| Time | f"{dt:%H:%M:%S}" | "09:30:00" |
| Custom object | format(obj, "spec") | Calls obj.__format__("spec") |
!r conversion | f"{obj!r}" | repr(obj) |
!s conversion | f"{obj!s}" | str(obj) |
!a conversion | f"{obj!a}" | ascii(obj) |
Implement __format__ on domain objects to make them directly embeddable in f-strings with custom specs.
Common gotchas
- F-strings evaluate at definition time, not call time; wrapping in a
lambdaor function defers evaluation. - Single-quoted strings inside
{}conflict with single-quoted f-strings in Python < 3.12. Use"outside or'''to wrap. formaton a float rounds to the display precision but does not change the stored value. Useround()if you need the rounded number.%with a single argument that is a tuple requires an extra wrap:"%s" % (value,), not"%s" % valuewhenvalueis itself a tuple.format_mapdoes not copy the mapping; mutations to the source dict during formatting are visible.- Locale-aware specifiers (
n) depend onlocale.setlocale. Prefer,or_for portable code.