Python String Formatting: f-strings vs format() vs % Operator - Which Should You Use?
I was reviewing some legacy Python code when I encountered this:
Name: %s, Age: %d, Score: %.2fWait, 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:
Hello {}, your score is {}And then f-strings scattered throughout newer code:
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
%sand%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:
┌─────────────────┬─────────────────────────────────────┬────────────────────┬─────────────────┐│ 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:
name = "Alice"age = 30score = 95.567
# Old printf-style formattingresult = "Name: %s, Age: %d, Score: %.2f" % (name, age, score)print(result)# Output: Name: Alice, Age: 30, Score: 95.57The problems I noticed:
- Not readable: I have to remember
%sis string,%dis integer,%fis float - Error-prone: If I mix up the order, I get wrong output or errors
- Deprecated: The Python docs say it “will eventually be removed from the language”
Here’s what happens when you get the order wrong:
# Wrong order - mismatched placeholdersname = "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 strSecond Attempt: str.format() (Better, But Verbose)
Then I tried the format() method:
name = "Alice"age = 30
# Basic usageresult = "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 templatetemplate = "{name} scored {score} points"print(template.format(name="Alice", score=95)) # Alice scored 95 pointsprint(template.format(name="Bob", score=87)) # Bob scored 87 pointsThis 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+):
name = "Alice"age = 30
# Simple and readableresult = f"Name: {name}, Age: {age}"print(result) # Name: Alice, Age: 30
# Expressions inline!a, b = 10, 20print(f"{a} + {b} = {a + b}") # 10 + 20 = 30
# Format specifiersprice = 19.99print(f"Price: ${price:.2f}") # Price: $19.99
# Debug mode (Python 3.8+)x = 42print(f"The value is {x=}") # The value is x=42This 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
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():
import timeit
name = "Alice"score = 95
# Timing testsfstring_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")f-string: 0.084521s ← Fastestformat(): 0.152637s% operator: 0.120845s3. Expressions Inline
You can compute directly in the string:
x = 10y = 20
# Math inlineprint(f"Sum: {x + y}, Product: {x * y}") # Sum: 30, Product: 200
# Method calls inlineitems = ["apple", "banana", "cherry"]print(f"Found {len(items)} items") # Found 3 items
# Dictionary accessdata = {"name": "Bob", "age": 25}print(f"{data['name']} is {data['age']}") # Bob is 25When 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:
# Define template onceemail_template = "Dear {recipient},\n\nYour appointment is on {date}.\n\nBest regards"
# Reuse many timesfor 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 with str.format() - no code executionuser_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:
┌──────────────────────┬──────────────────────────────┬───────────────────────────────┐│ 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:
value = 42.123
# Precisionprint(f"{value:.2f}") # "42.12"
# Width and alignmentprint(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 formattingval = 255print(f"{val:d}") # "255" (decimal)print(f"{val:x}") # "ff" (hexadecimal)print(f"{val:b}") # "11111111" (binary)
# Percentageratio = 0.856print(f"{ratio:.2%}") # "85.60%"Common Mistakes I Made
Mistake 1: Forgetting the f Prefix
name = "Alice"
# Wrong - this is just a literal stringprint("{name} says hello") # {name} says hello
# Correct - f prefix requiredprint(f"{name} says hello") # Alice says helloMistake 2: Using % Formatting in New Code
# Don't do this in new codeprint("Error: %s" % error_message)
# Do this insteadprint(f"Error: {error_message}")Mistake 3: Complex Expressions in f-strings
f-strings support expressions, but keep them readable:
# Too complex - hard to readprint(f"Result: {sum(x * y for x in range(10) for y in range(10) if x * y % 2 == 0)}")
# Better - compute outsideresult = 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!
# DANGEROUS - user could inject codeuser_template = input("Format: ")result = f"{user_template}" # Never do this!
# SAFE - use str.format() insteaduser_template = input("Format: ")result = user_template.format(name="value") # No code executionDecision Flowchart
When should you use each method?
┌─────────────────────────────┐ │ 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) │ └──────────────────────────────────────────────────────────┘Related Knowledge
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:
x = 10y = 20
# The = suffix prints both variable name and valueprint(f"{x=}, {y=}") # x=10, y=20print(f"{x + y=}") # x + y=30This is incredibly useful for debugging—no more typing print(f"x = {x}").
Performance Benchmarks
I ran some basic performance tests:
┌─────────────────────┬──────────────┬─────────────────────┐│ 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:
- Default to f-strings for 95% of string formatting—readable, fast, and modern
- Use
str.format()for reusable templates or user-supplied format strings - 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