CIS 2020 · Python · Reference

f-strings vs. % formatting

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.

The % operator: Python's original string formatting

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.

The conversion codes you'll actually run into.
CodeConvertsExample
%sAny value, as a string"%s" % 42 → '42'
%dInteger"%d" % 7 → '7'
%fFloating point, 6 decimals by default"%f" % 3.1 → '3.100000'
%.2fFloating point, 2 decimals"%.2f" % 3.14159 → '3.14'
%xInteger, as lowercase hexadecimal"%x" % 255 → 'ff'
%%A literal percent sign"%d%%" % 50 → '50%'

One value doesn't need a tuple

With exactly one value, the parentheses are optional:

print("Hello, %s." % name)
# Hello, Maya. -- a bare value works fine with a single placeholder

Named placeholders with a dictionary

% 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.

f-strings: formatted string literals

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.

Any expression is allowed, not just a variable name

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

Format specifiers: the part after the colon

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

The debugging shortcut: =

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

The same output, both ways

Every row below produces an identical string. The right-hand column is almost always shorter and reads closer to the sentence it actually prints.

% formatting and the equivalent f-string, for the same result.
Goal% formattingf-string
Insert a name"Hi, %s" % namef"Hi, {name}"
Insert two values"%s is %d" % (name, age)f"{name} is {age}"
2 decimal places"%.2f" % pricef"{price:.2f}"
Hexadecimal"%x" % 255f"{255:x}"
Use an expression"%d" % (x + y)f"{x + y}"
Call a method"%s" % name.upper()f"{name.upper()}"

Why f-strings won

Three real advantages, not just newer-is-better.

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.

Gotchas on both sides

% formatting: a single tuple value is ambiguous

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.

f-strings: the variable has to already exist

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.

f-strings: quoting inside the braces

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.

Quick check

If you remember only these

  • % formatting is the oldest style: a template string with %s/%d/%f placeholders, applied against a tuple or dictionary of values with the % operator.
  • An f-string is a string literal written as f"...", where anything inside { } is a live Python expression, evaluated immediately, not a placeholder filled in from somewhere else.
  • f-strings support the same formatting controls as %, written after a colon: f"{value:.2f}" is the f-string version of "%.2f" % value.
  • f-strings read better, since the value sits exactly where it prints, can hold a full expression instead of just a value, and run faster because they're compiled once instead of re-parsed every time.
  • "%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.
  • An f-string referencing an undefined name raises NameError immediately, the same as any other line of code that uses that name.