Question
Python String Formatting: %, str.format(), and f-Strings
Question
Python offers several ways to insert values into strings:
name = "Alice"
"Hello %s" % name
"Hello {0}".format(name)
f"Hello {name}"
Named values can also be used:
"Hello %(kwarg)s" % {"kwarg": name}
"Hello {kwarg}".format(kwarg=name)
f"Hello {name}"
All of these examples produce the same text. What are the differences between percent (%) formatting, str.format(), and f-strings? Which approach is best in different situations?
Also, when is formatting evaluated, and how can unnecessary runtime work be avoided, especially in performance-sensitive code?
Short Answer
Python has three common formatting styles: percent formatting, str.format(), and f-strings. For new Python code, f-strings are usually the clearest choice when the values are already available in the current scope. Use str.format() when the format string is stored separately or selected dynamically, and keep percent formatting when working with existing code or APIs such as Python's logging calls. The important performance detail is that ordinary formatting creates the final string immediately; it is not automatically lazy.
Concept
String formatting builds text by combining fixed characters with values such as names, numbers, dates, and objects.
Python supports three major styles:
- Percent formatting:
"Hello %s" % name str.format():"Hello {}".format(name)- Formatted string literals (f-strings):
f"Hello {name}"
All three can produce the same result, but they differ in readability, available syntax, compatibility, and whether the template can be chosen at runtime.
For most modern Python code (Python 3.6 and later), f-strings are the default recommendation. They put the expression next to the text where it is used:
user = "Alice"
message = f"Hello {user}"
An f-string can contain Python expressions, not only variable names:
price = 19.995
quantity = 3
receipt = f"Total: ${price * quantity:.2f}"
str.format() remains useful because its format string is normal data. It can be loaded from a configuration file, selected from a dictionary, or passed to a helper function before values are supplied.
Percent formatting is older, but it is not invalid or limited to old Python versions. It is still supported and is widely seen in existing projects. It also has an important role in the standard API, whose conventional calls use placeholders and can defer formatting.
Mental Model
Think of a format string as a form with blank spaces to fill in.
- With percent formatting, the blanks are marked using older labels such as
%sand%d. - With
str.format(), the blanks use braces such as{}or{name}. - With an f-string, you write the form directly where the values are visible, and braces contain Python expressions such as
{name.upper()}.
The key timing rule is simple: when Python reaches a normal formatting expression, it fills in the form immediately and creates a new string. If nobody will use that string, the work was unnecessary.
Logging is like writing a message on a card only if someone asks to read it. Parameterized logging can hold the template and values separately until the logger decides the message is needed.
Syntax and Examples
All styles can format values, but their placeholder syntax differs.
name = "Alice"
age = 30
price = 12.5
# Percent formatting
old_style = "Name: %s, age: %d, price: $%.2f" % (name, age, price)
# str.format()
format_style = "Name: {}, age: {}, price: ${:.2f}".format(name, age, price)
# f-string
f_string_style = f"Name: {name}, age: {age}, price: ${price:.2f}"
print(old_style)
print(format_style)
print(f_string_style)
Each line prints:
Name: Alice, age: 30, price: $12.50
Named placeholders
Names make long templates easier to read and make argument order less important.
name = "Alice"
score = 97
print("%(name)s earned %(score)d points" % {"name": name, "score": score})
print("{name} earned {score} points".format(name=name, score=score))
print(f"{name} earned points")
Step by Step Execution
Consider this f-string:
name = "Alice"
points = 42
message = f"{name} has {points + 8} points."
Execution happens in this order:
nameis assigned the string"Alice".pointsis assigned the integer42.- Python reaches the f-string expression.
- Python evaluates
{name}, producing"Alice". - Python evaluates
{points + 8}, producing50. - Python converts those values to text and creates one new string.
messagebecomes:
"Alice has 50 points."
The same immediate behavior applies to the other styles:
name = "Alice"
message = "Hello %s" % name
message = "Hello {}".format(name)
Real World Use Cases
User-facing messages
Use an f-string when assembling a message from values already in scope.
order_id = 1042
customer = "Alice"
message = f"Order #{order_id} is ready for {customer}."
Numbers in reports and receipts
Format specifications make numerical output readable.
subtotal = 1250.5
print(f"Subtotal: ${subtotal:,.2f}")
Reusable or dynamically selected templates
Use str.format() when a template is data rather than a literal written directly in the source code.
templates = {
"welcome": "Welcome, {name}!",
"farewell": "Goodbye, {name}!"
}
kind = "welcome"
message = templates[kind].format(name="Alice")
An f-string cannot replace a variable inside a string at runtime. This does not evaluate the braces:
# This remains the literal text "Welcome, {name}!"
template =
Real Codebase Usage
In current Python applications, developers commonly follow these patterns:
Use f-strings for local, readable messages
def build_profile_url(user_id: int) -> str:
return f"/users/{user_id}/profile"
Use a named str.format() template when the text is reusable
EMAIL_SUBJECT = "Your order {order_id} has shipped"
subject = EMAIL_SUBJECT.format(order_id=1042)
Named placeholders are especially useful for messages that may be translated, because translators can move placeholders without changing the argument order.
Use parameterized logging rather than eager f-strings
logger.debug("Processed %s records from %s", count, filename)
Avoid this in a hot path when debug logging is disabled:
logger.debug(f"Processed {count} records from {filename}")
The f-string is built before logger.debug() can decide to ignore it.
Common Mistakes
Assuming f-strings work as dynamic templates
An f-string must be marked with f in source code. A string stored in a variable is not reinterpreted later.
name = "Alice"
template = "Hello {name}"
print(template) # Hello {name}
Use format() for this case:
print(template.format(name=name))
Formatting too early in logging
This eagerly creates a string:
logger.debug(f"User data: {user_data}")
Prefer parameterized logging:
logger.debug("User data: %s", user_data)
If obtaining user_data itself is expensive, use an isEnabledFor() guard before calculating it.
Mixing formatting styles
Do not combine placeholder styles in one formatting operation.
.()
Comparisons
| Feature | Percent formatting | str.format() | f-strings |
|---|---|---|---|
| Basic syntax | "%s" % value | "{}".format(value) | f"{value}" |
| Available in | Python 2 and Python 3 | Python 2.6+ and Python 3 | Python 3.6+ |
| Readability in new code | Usually less clear | Clear, especially for templates | Usually clearest |
| Can evaluate expressions directly | No | Limited to field access/indexing in placeholders | Yes, such as {price * quantity} |
| Template can be stored or selected at runtime | Yes |
Cheat Sheet
name = "Alice"
price = 1234.5
# Recommended for normal new code
f"Hello, {name}!"
f"Price: ${price:,.2f}"
f"Next year: {2025 + 1}"
# Useful for a reusable/dynamic template
template = "Hello, {name}!"
template.format(name=name)
"Price: ${price:,.2f}".format(price=price)
# Legacy style and logging convention
"Hello, %s!" % name
"Price: $%.2f" % price
- Formatting normally happens immediately when the expression executes.
- Use
f"..."for new, local formatting in Python 3.6+. - Use
template.format(...)when the template is a variable or comes from outside the current source line. - Use
logger.debug("Value: %s", value)for deferred message rendering in standard logging. logger.debug(f"Value: {value}")builds the f-string before the logger checks its level.:.2fmeans two digits after the decimal point.:,adds thousands separators.!rin an f-string uses : .
FAQ
Which Python string formatting method should I use?
For most new Python 3.6+ code, use f-strings. They are concise and readable. Use str.format() when the template is dynamic or stored separately, and use % formatting for existing code or parameterized logging.
Are f-strings faster than str.format()?
F-strings are often efficient and commonly faster in simple benchmarks, but readability and correct program design matter more than tiny differences. Measure performance in your real application before optimizing.
When are f-strings evaluated?
They are evaluated when Python executes the line containing the f-string. Expressions inside {} run immediately.
Do f-strings avoid work when logging is disabled?
No. logger.debug(f"value={value}") builds the string before calling the logger. Use logger.debug("value=%s", value) to let standard logging defer rendering.
Can I use an f-string stored in a variable as a template?
No. template = "Hello {name}" is an ordinary string. Use template.format(name=name) if the placeholders need to be filled later.
Is percent formatting deprecated in Python?
No. It is still supported. However, f-strings and str.format() are generally preferred for new application code, except that %s-style placeholders remain common in logging.
Mini Project
Description
Build a small order-summary formatter for a command-line shop. It demonstrates f-strings for local output, format specifications for currency, and str.format() for a reusable message template.
Goal
Create a receipt message that validates an order and displays a customer name, item count, subtotal, discount, and final total.
Requirements
Use an f-string to format currency with commas and two decimal places.
Reject a negative subtotal or a discount outside the range 0 through 100.
Calculate the discount amount and final total.
Use a named str.format() template for the final status message.
Return the completed receipt as a string.
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Add Rows to a Pandas DataFrame in Python
Learn how to add rows to a Pandas DataFrame, why repeated row appends are slow, and when to use loc, concat, or record lists.
Call a Function by Name in a Python Module
Learn how to call a function by name in a Python module using strings, getattr, and safe patterns for dynamic function dispatch.