Python f-strings: Formatted String Literals Guide
Python 3.6 introduced f-strings (formatted string literals) as a more readable and performant alternative to existing string formatting methods. If you're still using %-formatting or str.format(),...
Key Insights
- F-strings are 20-50% faster than str.format() and significantly more readable than %-formatting, making them the preferred string formatting method in modern Python
- The
=specifier (Python 3.8+) enables self-documenting expressions likef"{variable=}"that print both the expression and its value, invaluable for debugging - F-strings evaluate expressions at runtime inside curly braces, allowing inline calculations, function calls, and complex formatting—but this power requires caution with user input to avoid injection attacks
Introduction to f-strings
Python 3.6 introduced f-strings (formatted string literals) as a more readable and performant alternative to existing string formatting methods. If you’re still using %-formatting or str.format(), it’s time to upgrade your code.
Here’s the evolution of Python string formatting:
# Old-style %-formatting (Python 2.x era)
name = "Alice"
age = 30
message = "Hello, %s. You are %d years old." % (name, age)
# str.format() method (Python 2.6+)
message = "Hello, {}. You are {} years old.".format(name, age)
message = "Hello, {name}. You are {age} years old.".format(name=name, age=age)
# f-strings (Python 3.6+)
message = f"Hello, {name}. You are {age} years old."
F-strings win on three fronts: they’re faster, more concise, and easier to read. The syntax is intuitive—prefix your string with f and embed expressions directly in curly braces. No more counting format specifiers or maintaining parallel argument lists.
Basic f-string Syntax and Usage
F-strings evaluate expressions at runtime, which means you can do more than just insert variables. You can perform calculations, call functions, and access object attributes directly within the string.
# Variable interpolation with different types
name = "Bob"
age = 25
height = 1.85
is_student = True
info = f"{name} is {age} years old, {height}m tall, student: {is_student}"
print(info)
# Output: Bob is 25 years old, 1.85m tall, student: True
# Inline expressions and calculations
price = 49.99
quantity = 3
total = f"Total: ${price * quantity}"
print(total) # Output: Total: $149.97
# Arithmetic operations
x = 10
y = 20
print(f"The sum of {x} and {y} is {x + y}")
# Output: The sum of 10 and 20 is 30
# Calling functions within f-strings
def get_discount(price):
return price * 0.1
original_price = 100
print(f"Price: ${original_price}, Discount: ${get_discount(original_price)}")
# Output: Price: $100, Discount: $10.0
# String methods
text = "hello world"
print(f"Uppercase: {text.upper()}, Length: {len(text)}")
# Output: Uppercase: HELLO WORLD, Length: 11
The key advantage is readability. When you read f"Hello, {name}", you immediately understand what’s happening. Compare this to "Hello, {}".format(name) where you need to match placeholders to arguments.
Formatting Specifications
F-strings support format specifiers that control how values are displayed. The syntax is {value:format_spec} where format_spec defines width, alignment, precision, and type.
# Number formatting - decimal precision
pi = 3.14159265359
print(f"Pi to 2 decimals: {pi:.2f}") # Output: Pi to 2 decimals: 3.14
print(f"Pi to 4 decimals: {pi:.4f}") # Output: Pi to 4 decimals: 3.1416
# Thousands separator
large_number = 1234567890
print(f"Formatted: {large_number:,}") # Output: Formatted: 1,234,567,890
print(f"With decimals: {large_number:,.2f}") # Output: With decimals: 1,234,567,890.00
# String alignment and padding
name = "Alice"
print(f"|{name:<10}|") # Left align, width 10: |Alice |
print(f"|{name:>10}|") # Right align, width 10: | Alice|
print(f"|{name:^10}|") # Center align, width 10: | Alice |
print(f"|{name:*^10}|") # Center with custom fill: |**Alice***|
# Number padding with zeros
number = 42
print(f"{number:05d}") # Output: 00042
# Percentage formatting
ratio = 0.8547
print(f"Success rate: {ratio:.2%}") # Output: Success rate: 85.47%
# Scientific notation
big_num = 123456789
print(f"Scientific: {big_num:.2e}") # Output: Scientific: 1.23e+08
# Date/time formatting
from datetime import datetime
now = datetime.now()
print(f"Date: {now:%Y-%m-%d}") # Output: Date: 2024-01-15
print(f"Time: {now:%H:%M:%S}") # Output: Time: 14:30:45
print(f"Full: {now:%Y-%m-%d %H:%M:%S}") # Output: Full: 2024-01-15 14:30:45
Format specifiers follow this pattern: [[fill]align][sign][#][0][width][,][.precision][type]. You don’t need to memorize this—learn the common patterns and reference the rest as needed.
Advanced f-string Features
Python 3.8 introduced the = specifier, which is a game-changer for debugging. It prints both the expression and its value:
# Self-documenting expressions (Python 3.8+)
x = 10
y = 20
print(f"{x=}, {y=}, {x+y=}")
# Output: x=10, y=20, x+y=30
# Extremely useful for debugging
def calculate_discount(price, discount_rate):
discount = price * discount_rate
final_price = price - discount
print(f"{price=}, {discount_rate=}, {discount=}, {final_price=}")
return final_price
calculate_discount(100, 0.15)
# Output: price=100, discount_rate=0.15, discount=15.0, final_price=85.0
You can nest f-strings for dynamic formatting, though readability can suffer:
# Nested f-strings for dynamic formatting
decimal_places = 2
value = 3.14159
print(f"Value: {value:.{decimal_places}f}") # Output: Value: 3.14
# Dynamic width and alignment
width = 10
align = ">"
text = "Hello"
print(f"{text:{align}{width}}") # Output: Hello
# Accessing nested data structures
user = {
"name": "Alice",
"details": {
"age": 30,
"city": "New York"
}
}
print(f"{user['name']} is {user['details']['age']} years old")
# Output: Alice is 30 years old
# Object attribute access
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Bob", 25)
print(f"{person.name} is {person.age} years old")
# Output: Bob is 25 years old
For multiline strings, you have two approaches:
# Multiline f-strings
name = "Alice"
age = 30
city = "Boston"
# Implicit concatenation
message = (
f"Name: {name}\n"
f"Age: {age}\n"
f"City: {city}"
)
# Triple-quoted f-string
message = f"""
Name: {name}
Age: {age}
City: {city}
"""
print(message)
Performance and Best Practices
F-strings are faster than alternatives because they’re evaluated at compile time rather than runtime. Here’s a performance comparison:
import timeit
name = "Alice"
age = 30
# Benchmark different formatting methods
def test_percent():
return "Hello, %s. You are %d years old." % (name, age)
def test_format():
return "Hello, {}. You are {} years old.".format(name, age)
def test_fstring():
return f"Hello, {name}. You are {age} years old."
print(f"%-formatting: {timeit.timeit(test_percent, number=1000000):.4f}s")
print(f"str.format(): {timeit.timeit(test_format, number=1000000):.4f}s")
print(f"f-strings: {timeit.timeit(test_fstring, number=1000000):.4f}s")
F-strings typically outperform str.format() by 20-50% and %-formatting by even more.
Critical security warning: Never use f-strings with untrusted user input for SQL queries or shell commands:
# DANGEROUS - SQL injection vulnerability
user_input = "admin' OR '1'='1"
query = f"SELECT * FROM users WHERE username = '{user_input}'"
# Results in: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# SAFE - Use parameterized queries
import sqlite3
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = ?", (user_input,))
To include literal curly braces in f-strings, double them:
# Escaping curly braces
value = 42
print(f"{{value}} = {value}") # Output: {value} = 42
print(f"CSS: {{ margin: {value}px; }}") # Output: CSS: { margin: 42px; }
Real-World Applications
F-strings excel in logging, report generation, and any scenario requiring formatted output:
# Structured logging
import logging
from datetime import datetime
logging.basicConfig(level=logging.INFO)
def process_order(order_id, user_id, amount):
timestamp = datetime.now()
logging.info(f"[{timestamp:%Y-%m-%d %H:%M:%S}] Processing order {order_id} "
f"for user {user_id}, amount: ${amount:.2f}")
process_order(12345, 'user_789', 99.99)
# Output: [2024-01-15 14:30:45] Processing order 12345 for user user_789, amount: $99.99
# Generating formatted reports/tables
def generate_sales_report(sales_data):
print(f"{'Product':<20} {'Units':>8} {'Revenue':>12}")
print("-" * 42)
for product, units, revenue in sales_data:
print(f"{product:<20} {units:>8} ${revenue:>11,.2f}")
total_revenue = sum(item[2] for item in sales_data)
print("-" * 42)
print(f"{'Total':<20} {'':<8} ${total_revenue:>11,.2f}")
sales = [
("Laptop", 45, 67500.00),
("Mouse", 230, 5750.00),
("Keyboard", 120, 9600.00)
]
generate_sales_report(sales)
# Email template rendering
def create_welcome_email(user_name, verification_code):
return f"""
Hi {user_name},
Welcome to our platform! Your verification code is: {verification_code}
This code will expire in 24 hours.
Best regards,
The Team
""".strip()
email = create_welcome_email("Alice", "ABC123")
print(email)
F-strings have become the standard for Python string formatting. They’re faster, more readable, and more Pythonic than alternatives. Use them everywhere except when dealing with untrusted input in security-sensitive contexts. Your code will be cleaner and your debugging sessions shorter.