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 logging API, whose conventional calls use %s placeholders and can defer formatting.
All three styles support format specifications for details such as decimal places, width, alignment, and thousands separators.
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 {score} points")
Format specifications
The part after : controls how a value is displayed. This format-specification language is shared by str.format() and f-strings.
value = 12345.6789
name = "Ada"
print(f"{value:.2f}") # 12345.68: two decimal places
print(f"{value:,.2f}") # 12,345.68: comma grouping
print(f"{name:>10}") # right-align in a field of width 10
print(f"{name:^10}") # center in a field of width 10
Expressions in f-strings
first_name = "alice"
items = ["book", "pen"]
print(f"Hello, {first_name.title()}!")
print(f"You have {len(items)} items.")
F-strings are evaluated by Python code, so use them only when the expression is understandable. For complicated logic, calculate the value first and format the result.
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)
In each case, formatting runs when that line executes. It does not wait until message is printed, logged, or otherwise used.
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 = "Welcome, {name}!"
Logging
For logging, pass the template and values separately when possible.
import logging
logger = logging.getLogger(__name__)
user_id = 42
logger.debug("Loading profile for user_id=%s", user_id)
This style lets the logging system avoid rendering the message when the DEBUG level is disabled.
SQL and shell commands
Do not use any string-formatting style to insert untrusted values into SQL or shell commands. Use the parameter mechanism provided by the database driver or subprocess API instead.
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.
Use a guard for expensive expressions
Parameterized logging delays conversion of arguments, but Python still evaluates function arguments before calling logger.debug. Guard truly expensive work:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Detailed payload: %s", build_detailed_payload())
Keep formatting separate from validation
Validate values first, then create the message. This gives clearer errors and avoids formatting invalid data accidentally.
def format_discount(percent: float) -> str:
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return f"Discount: {percent:.0f}%"
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.
# Broken: .format() only understands braces, not %s
"Hello %s".format("Alice")
Use one complete style:
"Hello {}".format("Alice")
Forgetting a tuple for multiple percent values
# Broken: the right side must supply both values
"%s has %d points" % "Alice"
"%s has %d points" % ("Alice", 42)
For one value that is itself a tuple, add a trailing comma:
coordinates = (10, 20)
print("Coordinates: %s" % (coordinates,))
Using the wrong conversion or format specification
%d expects an integer-like value, while %s converts with str().
# May fail because "42" is a string, not an integer
"Count: %d" % "42"
With f-strings, use suitable formatting for the actual type:
count = int("42")
print(f"Count: {count:d}")
Using formatting to make SQL queries
# Unsafe: never build a query this way
query = f"SELECT * FROM users WHERE name = '{name}'"
Use your database library's parameterized query syntax. The exact placeholder style depends on the driver.
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 | Yes | No; f-string syntax is part of source code |
| Named values | %(name)s with a mapping | {name} with keyword arguments or a mapping | {name} from the current scope |
| Common logging convention | Yes | Not the standard logging style | Works, but is eager |
| Best default for new local code | Usually no | Sometimes | Yes |
A practical choice guide
- Choose f-strings for ordinary Python code that creates text from nearby variables.
- Choose
str.format()for reusable, configurable, or dynamically selected templates. - Choose percent formatting when maintaining existing code or using parameterized logging calls.
str.format() and f-strings share most format specifications:
number = 3.14159
print("{:.2f}".format(number))
print(f"{number:.2f}")
Both print 3.14.
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 usesrepr():f"value={value!r}".- Escape literal braces in f-strings and
str.format()with doubled braces:f"{{example}}". - Never format untrusted values directly into SQL or shell commands.
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.
How do I print literal braces in an f-string?
Double them:
print(f"Use {{braces}} around a value: {42}")
Should I use string formatting to build SQL queries?
No. Use parameterized queries from your database library. This prevents quoting errors and SQL injection vulnerabilities.
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.