Question

Python String Formatting: %, str.format(), and f-Strings

pythonperformancestring-formattingf-string

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

Take Quiz

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:

  1. name is assigned the string "Alice".
  2. points is assigned the integer 42.
  3. Python reaches the f-string expression.
  4. Python evaluates {name}, producing "Alice".
  5. Python evaluates {points + 8}, producing 50.
  6. Python converts those values to text and creates one new string.
  7. message becomes:
"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.

Take Quiz

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

FeaturePercent formattingstr.format()f-strings
Basic syntax"%s" % value"{}".format(value)f"{value}"
Available inPython 2 and Python 3Python 2.6+ and Python 3Python 3.6+
Readability in new codeUsually less clearClear, especially for templatesUsually clearest
Can evaluate expressions directlyNoLimited to field access/indexing in placeholdersYes, such as {price * quantity}
Template can be stored or selected at runtimeYesYesNo; 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 conventionYesNot the standard logging styleWorks, but is eager
Best default for new local codeUsually noSometimesYes

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.

Take Quiz

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.
  • :.2f means two digits after the decimal point.
  • :, adds thousands separators.
  • !r in an f-string uses repr(): 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more