CIS 2020 · Python · Reference
Python has had more than one way to build a string out of variables for a long time. If you've seen "Hello, %s" % name in older code and f"Hello, {name}" in newer code and wondered whether they're the same thing, they're not quite, and knowing the difference saves real confusion the first time one of them does something unexpected.
Borrowed almost directly from C's printf, and in every version of Python there has ever been.
% formatting works by writing a template string with placeholders, then applying the % operator between that string and the values that fill the placeholders:
name = "Maya"
score = 91
print("Hello, %s. Your score is %d." % (name, score))
# Hello, Maya. Your score is 91.
Each placeholder starts with % and ends with a conversion code that says what type of value belongs there: %s for a string, %d for an integer, %f for a floating point number, and a handful of others. The values on the right side of % are supplied as a tuple, in the same order the placeholders appear.
| Code | Converts | Example |
|---|---|---|
| %s | Any value, as a string | "%s" % 42 → '42' |
| %d | Integer | "%d" % 7 → '7' |
| %f | Floating point, 6 decimals by default | "%f" % 3.1 → '3.100000' |
| %.2f | Floating point, 2 decimals | "%.2f" % 3.14159 → '3.14' |
| %x | Integer, as lowercase hexadecimal | "%x" % 255 → 'ff' |
| %% | A literal percent sign | "%d%%" % 50 → '50%' |
With exactly one value, the parentheses are optional:
print("Hello, %s." % name)
# Hello, Maya. -- a bare value works fine with a single placeholder
% formatting also accepts a dictionary instead of a tuple, naming each placeholder instead of relying on position:
data = {"name": "Maya", "score": 91}
print("Hello, %(name)s. Your score is %(score)d." % data)
# Hello, Maya. Your score is 91.
Added in Python 3.6 (2016), and the way most Python written today actually builds strings.
An f-string is a string literal with an f immediately before the opening quote. Inside the string, anything inside curly braces { } is not text, it is a real Python expression, evaluated on the spot and dropped into the string:
name = "Maya"
score = 91
print(f"Hello, {name}. Your score is {score}.")
# Hello, Maya. Your score is 91.
There is no conversion code to remember and no separate tuple of values to keep lined up with placeholders; the variable's own name goes directly inside the braces, right where it will appear.
Because what's inside { } is ordinary Python, it can be arithmetic, a method call, indexing, anything that produces a value:
price = 19.999
quantity = 3
print(f"Total: {price * quantity}")
# Total: 59.997
print(f"Name in caps: {name.upper()}")
# Name in caps: MAYA
f-strings still support the same kind of formatting control % formatting has, written after a colon inside the braces:
print(f"Total: ${price * quantity:.2f}")
# Total: $59.997 becomes $60.00, rounded and padded to two decimals
print(f"{255:x}")
# ff -- hexadecimal, same idea as %x
print(f"{7:04d}")
# 0007 -- padded to 4 digits with leading zeros
=Since Python 3.8, adding = right before the closing brace prints both the expression's source text and its value, which is a fast way to check what a variable actually holds while debugging:
print(f"{score=}")
# score=91
Every row below produces an identical string. The right-hand column is almost always shorter and reads closer to the sentence it actually prints.
| Goal | % formatting | f-string |
|---|---|---|
| Insert a name | "Hi, %s" % name | f"Hi, {name}" |
| Insert two values | "%s is %d" % (name, age) | f"{name} is {age}" |
| 2 decimal places | "%.2f" % price | f"{price:.2f}" |
| Hexadecimal | "%x" % 255 | f"{255:x}" |
| Use an expression | "%d" % (x + y) | f"{x + y}" |
| Call a method | "%s" % name.upper() | f"{name.upper()}" |
Three real advantages, not just newer-is-better.
{price * quantity}, with nowhere else in the line to keep synchronized.The official style guide agrees. PEP 8 and the core Python documentation both point to f-strings as the preferred way to format strings in modern Python, specifically because of the readability and expression-support points above.
If the one value you're substituting is itself a tuple, % gets confused about whether that tuple is "the one value" or "several values":
point = (3, 4)
print("Point: %s" % point)
# TypeError: not all arguments converted during string formatting
print("Point: %s" % (point,))
# Point: (3, 4) -- wrapping it in a one-item tuple fixes it
This exact error confuses almost everyone the first time. Python saw (3, 4) on the right of % and assumed it meant two separate values for two placeholders, not one placeholder holding a tuple. Wrapping the whole thing in an extra (value,) tells Python there is exactly one value, which happens to be a tuple.
Because the code inside { } runs immediately, referencing a name that was never assigned raises the same NameError a normal line of code would:
print(f"{undefined_variable}")
# NameError: name 'undefined_variable' is not defined
This is usually a feature, not a bug, since it surfaces a typo immediately instead of silently printing the wrong thing, but it is a real difference from % formatting against a dictionary, which raises KeyError instead, naming the missing key directly.
In Python versions before 3.12, an f-string could not reuse the exact same quote character both outside the f-string and inside its braces:
data = {"name": "Maya"}
# Before Python 3.12, this line is a SyntaxError:
# print(f"{data["name"]}")
print(f"{data['name']}")
# Maya -- switch to single quotes inside, double quotes outside
Python 3.12 removed this restriction, but plenty of installed Python interpreters are still older than that, so mixing quote styles, double quotes outside and single quotes inside, remains the safe habit.
% formatting is the oldest style: a template string with %s/%d/%f placeholders, applied against a tuple or dictionary of values with the % operator.f"...", where anything inside { } is a live Python expression, evaluated immediately, not a placeholder filled in from somewhere else.%, written after a colon: f"{value:.2f}" is the f-string version of "%.2f" % value."%s" % a_tuple is a classic trap; wrap a single tuple value as (a_tuple,) so Python doesn't mistake its contents for multiple substitution values.NameError immediately, the same as any other line of code that uses that name.