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 like f"{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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.