Skip to content

Python String Formatting: f-strings vs format() vs % Operator - Which Should You Use?

I was reviewing some legacy Python code when I encountered this:

Console Output
Name: %s, Age: %d, Score: %.2f

Wait, what does %.2f mean again? And why are there %s and %d placeholders? I had to look it up—it’s the old printf-style formatting from C.

Then I saw str.format() in other files:

Console Output
Hello {}, your score is {}

And then f-strings scattered throughout newer code:

Console Output
Hello {name}, your score is {score}

Three different ways to do the same thing. Which one should I use? And why does Python have so many string formatting methods?

The Problem: Too Many Options

Python has evolved over the years, and string formatting is a prime example. If you’re confused by %s, format(), and f-strings, you’re not alone.

Here’s what confused me:

  • Which one is “correct” for modern Python?
  • Why does older code use %s and %d?
  • What’s the performance difference?
  • When should I use each method?

A Quick Comparison

Before diving deep, here’s a side-by-side comparison of the three methods formatting the same string:

String Formatting Comparison
┌─────────────────┬─────────────────────────────────────┬────────────────────┬─────────────────┐
│ Method │ Syntax │ Output │ Status │
├─────────────────┼─────────────────────────────────────┼────────────────────┼─────────────────┤
│ f-strings │ f"{name} is {age} years old" │ Alice is 30... │ ✓ Recommended │
│ str.format() │ "{} is {} years old".format(...) │ Alice is 30... │ Use for templates│
│ % operator │ "%s is %d years old" % (...) │ Alice is 30... │ ✗ Deprecated │
└─────────────────┴─────────────────────────────────────┴────────────────────┴─────────────────┘

The answer surprised me: f-strings are the modern standard, and the % operator will eventually be removed from the language.

Trial and Error: Testing Each Method

First Attempt: The % Operator (What I Saw in Legacy Code)

I started by understanding the old syntax:

old_formatting.py
name = "Alice"
age = 30
score = 95.567
# Old printf-style formatting
result = "Name: %s, Age: %d, Score: %.2f" % (name, age, score)
print(result)
# Output: Name: Alice, Age: 30, Score: 95.57

The problems I noticed:

  1. Not readable: I have to remember %s is string, %d is integer, %f is float
  2. Error-prone: If I mix up the order, I get wrong output or errors
  3. Deprecated: The Python docs say it “will eventually be removed from the language”

Here’s what happens when you get the order wrong:

formatting_error.py
# Wrong order - mismatched placeholders
name = "Alice"
age = 30
# This will fail if types don't match
# print("%d is %s" % (name, age)) # TypeError: %d format: a real number is required, not str

Second Attempt: str.format() (Better, But Verbose)

Then I tried the format() method:

format_method.py
name = "Alice"
age = 30
# Basic usage
result = "Name: {}, Age: {}".format(name, age)
print(result) # Name: Alice, Age: 30
# With named placeholders (more readable)
result = "Name: {name}, Age: {age}".format(name=name, age=age)
print(result) # Name: Alice, Age: 30
# Reusable template
template = "{name} scored {score} points"
print(template.format(name="Alice", score=95)) # Alice scored 95 points
print(template.format(name="Bob", score=87)) # Bob scored 87 points

This is better—named placeholders are clear. But typing .format(name=name, age=age) is verbose.

Third Attempt: f-strings (The Modern Way)

Finally, I tried f-strings (Python 3.6+):

fstrings.py
name = "Alice"
age = 30
# Simple and readable
result = f"Name: {name}, Age: {age}"
print(result) # Name: Alice, Age: 30
# Expressions inline!
a, b = 10, 20
print(f"{a} + {b} = {a + b}") # 10 + 20 = 30
# Format specifiers
price = 19.99
print(f"Price: ${price:.2f}") # Price: $19.99
# Debug mode (Python 3.8+)
x = 42
print(f"The value is {x=}") # The value is x=42

This is much cleaner. The variable name is right there in the string—no need to remember %s vs %d.

Why f-strings Are Better

The Python Tutorial explains that f-strings “let you include the value of Python expressions inside a string by prefixing the string with f or F.”

Here’s why I prefer f-strings:

1. Readability

Readability Comparison
Old: "Hello %s, your score is %d" % (name, score)
Format: "Hello {name}, your score is {score}".format(name=name, score=score)
f-string: f"Hello {name}, your score is {score}"
→ f-strings are the most readable—variables are directly visible in the string.

2. Performance

f-strings are evaluated at runtime and are faster than format():

performance_test.py
import timeit
name = "Alice"
score = 95
# Timing tests
fstring_time = timeit.timeit('f"Hello {name}, score: {score}"', globals=globals())
format_time = timeit.timeit('"Hello {}, score: {}".format(name, score)', globals=globals())
percent_time = timeit.timeit('"Hello %s, score: %d" % (name, score)', globals=globals())
print(f"f-string: {fstring_time:.6f}s")
print(f"format(): {format_time:.6f}s")
print(f"% operator: {percent_time:.6f}s")
Performance Results
f-string: 0.084521s ← Fastest
format(): 0.152637s
% operator: 0.120845s

3. Expressions Inline

You can compute directly in the string:

inline_expressions.py
x = 10
y = 20
# Math inline
print(f"Sum: {x + y}, Product: {x * y}") # Sum: 30, Product: 200
# Method calls inline
items = ["apple", "banana", "cherry"]
print(f"Found {len(items)} items") # Found 3 items
# Dictionary access
data = {"name": "Bob", "age": 25}
print(f"{data['name']} is {data['age']}") # Bob is 25

When to Use str.format() Instead

There are cases where str.format() is the better choice:

1. Reusable Templates

If you use the same format string multiple times:

reusable_template.py
# Define template once
email_template = "Dear {recipient},\n\nYour appointment is on {date}.\n\nBest regards"
# Reuse many times
for user in users:
message = email_template.format(recipient=user.name, date=user.appointment)
send_email(message)

2. User-Supplied Format Strings

Critical for security: Never use f-strings with user input!

safe_format.py
# SAFE with str.format() - no code execution
user_format = input("Enter format: ") # e.g., "{item}: ${price}"
result = user_format.format(item="Widget", price=9.99)
# DANGEROUS with f-strings - can execute arbitrary code!
# user_input = "__import__('os').system('rm -rf /')"
# f"{user_input}" # NEVER do this!

f-strings evaluate Python expressions. If you let users provide format strings, they could inject malicious code. str.format() only replaces placeholders—no code execution.

Format Specifiers Reference

All three methods support format specifiers, but f-strings have the cleanest syntax:

Format Specifiers Comparison
┌──────────────────────┬──────────────────────────────┬───────────────────────────────┐
│ Purpose │ f-string │ format() │
├──────────────────────┼──────────────────────────────┼───────────────────────────────┤
│ 2 decimal places │ f"{price:.2f}" │ "{:.2f}".format(price) │
│ Right-align (10 ch) │ f"{price:>10.2f}" │ "{:>10.2f}".format(price) │
│ Left-align (10 ch) │ f"{price:<10.2f}" │ "{:<10.2f}".format(price) │
│ Center (10 ch) │ f"{price:^10.2f}" │ "{:^10.2f}".format(price) │
│ Hexadecimal │ f"{val:x}" │ "{:x}".format(val) │
│ Binary │ f"{val:b}" │ "{:b}".format(val) │
│ Percentage │ f"{ratio:.2%}" │ "{:.2%}".format(ratio) │
└──────────────────────┴──────────────────────────────┴───────────────────────────────┘

Example usage:

format_specifiers.py
value = 42.123
# Precision
print(f"{value:.2f}") # "42.12"
# Width and alignment
print(f"{value:10.2f}") # " 42.12" (right-aligned)
print(f"{value:<10.2f}") # "42.12 " (left-aligned)
print(f"{value:^10.2f}") # " 42.12 " (centered)
# Integer formatting
val = 255
print(f"{val:d}") # "255" (decimal)
print(f"{val:x}") # "ff" (hexadecimal)
print(f"{val:b}") # "11111111" (binary)
# Percentage
ratio = 0.856
print(f"{ratio:.2%}") # "85.60%"

Common Mistakes I Made

Mistake 1: Forgetting the f Prefix

mistake_no_f.py
name = "Alice"
# Wrong - this is just a literal string
print("{name} says hello") # {name} says hello
# Correct - f prefix required
print(f"{name} says hello") # Alice says hello

Mistake 2: Using % Formatting in New Code

mistake_percent.py
# Don't do this in new code
print("Error: %s" % error_message)
# Do this instead
print(f"Error: {error_message}")

Mistake 3: Complex Expressions in f-strings

f-strings support expressions, but keep them readable:

mistake_complex.py
# Too complex - hard to read
print(f"Result: {sum(x * y for x in range(10) for y in range(10) if x * y % 2 == 0)}")
# Better - compute outside
result = sum(x * y for x in range(10) for y in range(10) if x * y % 2 == 0)
print(f"Result: {result}")

Mistake 4: Using f-strings for User Templates

Never use f-strings with user-supplied format strings—security risk!

mistake_security.py
# DANGEROUS - user could inject code
user_template = input("Format: ")
result = f"{user_template}" # Never do this!
# SAFE - use str.format() instead
user_template = input("Format: ")
result = user_template.format(name="value") # No code execution

Decision Flowchart

When should you use each method?

Decision Flowchart
┌─────────────────────────────┐
│ Need string formatting? │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Is format string user-supplied?│
└──────────────┬──────────────┘
┌──────────────┴──────────────┐
│ │
YES NO
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ Use str.format() │ │ Reusing same template │
│ (Safe - no code │ │ multiple times? │
│ execution) │ └───────────┬─────────────┘
└─────────────────────┘ │
┌──────────┴──────────┐
│ │
YES NO
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Use str.format() │ │ Use f-strings │
│ with template │ │ (Best for most │
└──────────────────┘ │ cases) │
└──────────────────┘
┌──────────────────────────────────────────────────────────┐
│ NEVER use % operator in new code (deprecated) │
└──────────────────────────────────────────────────────────┘

Python Version Compatibility

  • f-strings: Python 3.6+ (most widely used now)
  • str.format(): Python 2.6+ and 3.x
  • % operator: All Python versions (but deprecated)

If you’re targeting Python 3.5 or older, you must use str.format() or % operator.

Debug Mode (Python 3.8+)

Python 3.8 added a debugging feature to f-strings:

debug_mode.py
x = 10
y = 20
# The = suffix prints both variable name and value
print(f"{x=}, {y=}") # x=10, y=20
print(f"{x + y=}") # x + y=30

This is incredibly useful for debugging—no more typing print(f"x = {x}").

Performance Benchmarks

I ran some basic performance tests:

Performance Benchmark (1 million iterations)
┌─────────────────────┬──────────────┬─────────────────────┐
│ Method │ Time │ Relative Speed │
├─────────────────────┼──────────────┼─────────────────────┤
│ f-strings │ ~0.08s │ Fastest (1.0x) │
│ % operator │ ~0.12s │ Slower (1.5x) │
│ str.format() │ ~0.15s │ Slowest (1.9x) │
│ str.format() + dict │ ~0.20s │ Slowest (2.5x) │
└─────────────────────┴──────────────┴─────────────────────┘

f-strings are nearly 2x faster than str.format() in this simple test. The difference becomes more significant in tight loops.

Final Thoughts

After understanding the history and capabilities of each method, here’s my approach:

  1. Default to f-strings for 95% of string formatting—readable, fast, and modern
  2. Use str.format() for reusable templates or user-supplied format strings
  3. Avoid % operator except when maintaining legacy code

The Python Tutorial (Chapter 7.1.4) explicitly states that old-style formatting “will eventually be removed from the language.” So if you’re still using %s and %d, now is the time to migrate.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments