Python String Formatting
Learn all three Python string formatting methods: % operator, str.format(), and f-strings, with examples for numbers, alignment, dates, and more.
String formatting lets you build strings dynamically by inserting variables, expressions, and computed values into a template. Python offers three distinct approaches — the legacy % operator, the versatile str.format() method, and the modern f-string syntax — each with its own strengths and appropriate use cases.
This chapter covers:
- When to use each formatting method and why
- The
%operator (legacy, still common in logs and older codebases) str.format()with positional and named placeholders- f-strings (the recommended modern approach)
- Format specifiers: width, alignment, precision, fill, sign, and base
- Formatting numbers, percentages, dates, and times
- Common gotchas and edge cases
For a deep dive into f-strings specifically — including expressions, conditional logic, and Python 3.12 improvements — see the Python f-Strings chapter.
Which Method Should You Use?
| Method | Python version | When to use it |
|---|---|---|
% operator | 2.x / 3.x | Legacy code, logging module, very simple substitutions |
str.format() | 2.6+ | When you need reusable format strings or keyword arguments |
| f-strings | 3.6+ | New code — fastest, most readable, supports expressions inline |
Unless you are maintaining Python 2 code or an older Python 3 codebase, use f-strings. They are evaluated at runtime, produce no extra call overhead, and keep the variable name next to where it appears in the string.
The % Operator (Legacy Formatting)
The % operator substitutes values into a format string using C-style conversion specifiers.
Common conversion specifiers:
| Specifier | Meaning |
|---|---|
%s | String (calls str() on the value) |
%d | Signed integer |
%f | Floating-point number |
%r | repr() of the value |
%% | A literal % character |
Width and Precision with %
You can control field width and decimal precision directly in the specifier:
pi = 3.14159265
print("%10.3f" % pi) # right-aligned in a 10-char field, 3 decimal places
# 3.142
print("%-10.3f|" % pi) # left-aligned
# 3.142 |
print("%010.3f" % pi) # zero-padded
# 000003.142Gotcha: Single Values vs. Tuples
When substituting a single value, passing a tuple is the safe approach:
value = "hello"
# Risky — if value were itself a tuple, this would fail:
# print("Got: %s" % value)
# Safe:
print("Got: %s" % (value,))
# Got: hellostr.format() — Flexible Placeholder Formatting
The str.format() method replaced the % operator in Python 2.6+. It uses {} as placeholders and supports positional, indexed, and keyword arguments.
Positional and Indexed Placeholders
Named (Keyword) Placeholders
Named placeholders make format strings self-documenting and are especially useful when the same value appears multiple times or when the format string comes from a config file.
Reusing Format Strings
Because str.format() takes a string as input, you can store the template in a variable and reuse it:
template = "Hello, {name}! You have {count} new messages."
print(template.format(name="Alice", count=3))
print(template.format(name="Bob", count=0))
# Hello, Alice! You have 3 new messages.
# Hello, Bob! You have 0 new messages.f-Strings — Modern Python Formatting
f-strings (formatted string literals) prefix the string with f or F and evaluate any expression inside {} at runtime. They are available from Python 3.6 onwards and are the recommended approach for new code.
name = "John"
age = 25
print(f"My name is {name} and I am {age} years old.")
# My name is John and I am 25 years old.f-strings support any valid Python expression inside the braces — arithmetic, method calls, conditionals, and more:
items = ["apple", "banana", "cherry"]
print(f"Cart has {len(items)} items. First: {items[0].upper()}.")
# Cart has 3 items. First: APPLE.
x = -7
print(f"Absolute value: {abs(x)}")
# Absolute value: 7See Python f-Strings for the full guide, including debugging with =, multiline f-strings, and Python 3.12 enhancements.
Format Specifiers: The Mini-Language
Both str.format() and f-strings use the same format specification mini-language inside the {} after a colon:
{[field_name]:[fill][align][sign][#][0][width][grouping][.precision][type]}The table below shows the most useful options:
| Option | Character | Effect |
|---|---|---|
| Alignment | < > ^ | Left, right, center |
| Fill | any char | Fills padding (used with alignment and width) |
| Sign | + - | Show + for positive; default minus only; space for positive |
| Prefix | # | 0x for hex, 0o for octal, 0b for binary |
| Zero-pad | 0 | Pad with zeros instead of spaces |
| Width | integer | Minimum field width |
| Grouping | , or _ | Thousands separator |
| Precision | .n | Decimal places (floats) or max chars (strings) |
| Type | d f e g x o b % | Integer, float, scientific, general, hex, octal, binary, percent |
Alignment and Width
text = "hello"
print(f"{text:<10}|") # left-aligned in 10-char field
# hello |
print(f"{text:>10}|") # right-aligned
# hello|
print(f"{text:^10}|") # centered
# hello |
print(f"{text:*^10}|") # centered, filled with *
# **hello***|The same alignment specifiers work with str.format(). Here is an example combining alignment and precision:
Number Formatting
x = 123.456789
print(f"{x:.2f}") # two decimal places
# 123.46
print(f"{x:,.2f}") # thousands separator, two decimal places
# 123.46
print(f"{x:+.2f}") # explicit plus sign for positive numbers
# +123.46
print(f"{x:10.2f}") # right-aligned in a 10-char field
# 123.46
print(f"{x:<10.2f}|") # left-aligned
# 123.46 |
print(f"{x:010.2f}") # zero-padded to 10 characters
# 0000123.46Large Numbers and Percentages
population = 8_100_000_000
print(f"{population:,}") # comma separator
# 8,100,000,000
print(f"{population:_}") # underscore separator (Python 3.6+)
# 8_100_000_000
ratio = 0.8567
print(f"{ratio:.1%}") # percentage, one decimal place
# 85.7%Integer Bases
n = 255
print(f"{n:d}") # decimal (default)
# 255
print(f"{n:x}") # lowercase hexadecimal
# ff
print(f"{n:X}") # uppercase hexadecimal
# FF
print(f"{n:#x}") # hex with 0x prefix
# 0xff
print(f"{n:o}") # octal
# 377
print(f"{n:b}") # binary
# 11111111
print(f"{n:#b}") # binary with 0b prefix
# 0b11111111You can also use the same specifiers with str.format():
Scientific Notation
avogadro = 6.02214076e23
print(f"{avogadro:.3e}") # scientific notation, 3 decimal places
# 6.022e+23
print(f"{avogadro:.3E}") # uppercase E
# 6.022E+23
print(f"{avogadro:.3g}") # general: compact form, removes trailing zeros
# 6.02e+23Formatting Strings with format() and f-Strings
All three approaches produce the same output for a simple substitution:
String-specific specifiers let you control truncation and padding:
s = "Python"
print(f"{s:.3}") # truncate to 3 characters
# Pyt
print(f"{s:10}") # pad to width 10 (left-aligned by default for strings)
# Python
print(f"{s:>10}") # right-aligned
# Python
print(f"{s:*^12}") # centered, filled with *
# ***Python***Formatting Dates and Times
Use strftime format codes inside the braces when formatting datetime objects with f-strings or str.format():
import datetime
date = datetime.datetime(2024, 3, 15, 10, 30, 0)
# Default string representation
print(f"Default: {date}")
# Default: 2024-03-15 10:30:00
# strftime codes inside the format spec
print(f"Formatted: {date:%B %d, %Y}")
# Formatted: March 15, 2024
print(f"Time only: {date:%H:%M:%S}")
# Time only: 10:30:00
print(f"ISO-style: {date:%Y-%m-%d}")
# ISO-style: 2024-03-15The same syntax works with str.format():
import datetime
date = datetime.datetime(2024, 3, 15, 10, 30, 0)
print("The date is {:%B %d, %Y}".format(date))
# The date is March 15, 2024For advanced date formatting, see the Python Modify Strings chapter which covers string manipulation techniques.
Advanced Techniques
Nested Expressions in f-Strings
f-strings allow you to compute the format specifier dynamically:
width = 10
precision = 3
value = 3.14159
print(f"{value:{width}.{precision}f}")
# output: 3.142 (right-aligned in a 10-char field, 3 decimal places)Format Spec with format() and Variables
The same technique works with str.format() using nested {}:
width = 8
print("{:{width}}".format("left", width=width))
# left Conditional Formatting in f-Strings
Since f-strings evaluate any expression, you can use conditional expressions inline:
score = 73
label = f"{'pass' if score >= 60 else 'fail'}"
print(f"Score {score}: {label}")
# Score 73: passDebugging with = (Python 3.8+)
Append = inside an f-string to print both the expression and its value — very useful for debugging:
x = 42
y = x * 2
print(f"{x=}, {y=}")
# x=42, y=84Common Gotchas
1. Brace escaping. To include a literal { or } in any formatted string, double it:
print(f"Use {{curly braces}} in f-strings")
# Use {curly braces} in f-strings2. Quotes inside f-strings. In Python 3.11 and earlier, the expression inside {} cannot use the same quote type as the outer f-string:
names = ["Alice", "Bob"]
# Wrong in Python <= 3.11:
# print(f"First: {names[0].upper()}") -- this is fine
# print(f"{'Alice'.upper()}") -- single quotes inside double-quoted f-string is fine
# But nesting the same quotes fails:
# print(f"{names["Alice"]}") -- SyntaxError in <= 3.11
# Safe approach for <= 3.11:
key = "Alice"
print(f"{key.upper()}")
# ALICE3. The % tuple trap. When using % formatting with a single value that is itself a tuple, wrap it:
coords = (10, 20)
# This raises TypeError because % sees a 2-element tuple:
# print("Position: %s" % coords)
# Fix: wrap in a 1-element tuple
print("Position: %s" % (coords,))
# Position: (10, 20)4. str.format() vs. security. Never pass untrusted user input as a format string to str.format() — a malicious template can access object attributes and leak data. f-strings are always hard-coded strings, so they are not vulnerable to this.
Practical Comparison
Here is the same output produced by all three methods side by side:
item = "widget"
qty = 42
unit_price = 4.5
# % operator
print("%-12s %4d $%7.2f" % (item, qty, unit_price))
# str.format()
print("{:<12} {:>4} ${:>7.2f}".format(item, qty, unit_price))
# f-string
print(f"{item:<12} {qty:>4} ${unit_price:>7.2f}")
# All print:
# widget 42 $ 4.50Summary
- Use f-strings for new Python 3.6+ code: they are the most readable and fastest.
- Use
str.format()when you need a reusable template string or must support Python 2.6+. - Use
%only in legacy code or when working with theloggingmodule (which defers formatting until the log entry is actually emitted). - The format specification mini-language (
width,precision,align,fill,type) applies the same way to bothstr.format()and f-strings.
Related chapters:
- Python f-Strings — deep dive into f-string expressions and Python 3.12 features
- Python Strings — string basics, indexing, and slicing
- Modify Strings — built-in string methods
- Escape Characters — special characters in strings