Python - String Formatting (f-strings, format, %)
• F-strings (formatted string literals) offer the fastest and most readable string formatting in Python 3.6+, with direct variable interpolation and expression evaluation inside curly braces.
Key Insights
• F-strings (formatted string literals) offer the fastest and most readable string formatting in Python 3.6+, with direct variable interpolation and expression evaluation inside curly braces.
• The str.format() method provides more flexibility for complex formatting scenarios, template reuse, and backwards compatibility with Python 2.7-3.5.
• Old-style % formatting remains useful for C-style format strings and logging but is generally superseded by newer methods in application code.
F-strings: Modern Python’s Default Choice
F-strings, introduced in Python 3.6, provide the most concise and performant string formatting method. Prefix any string with f or F to enable interpolation.
name = "Alice"
age = 30
balance = 1234.5678
# Basic interpolation
message = f"Hello, {name}!"
# Output: Hello, Alice!
# Expressions inside f-strings
result = f"{name} will be {age + 1} next year"
# Output: Alice will be 31 next year
# Method calls and attributes
text = "python"
formatted = f"{text.upper()} has {len(text)} letters"
# Output: PYTHON has 6 letters
F-strings support format specifications using the same syntax as str.format(). Place a colon after the expression followed by format specifiers.
# Number formatting
pi = 3.14159265359
print(f"{pi:.2f}") # 3.14 (2 decimal places)
print(f"{pi:.4f}") # 3.1416 (4 decimal places)
# Padding and alignment
name = "Bob"
print(f"{name:>10}") # " Bob" (right-align, width 10)
print(f"{name:<10}") # "Bob " (left-align, width 10)
print(f"{name:^10}") # " Bob " (center, width 10)
print(f"{name:*^10}") # "***Bob****" (center with * padding)
# Number formatting with thousands separator
large_num = 1000000
print(f"{large_num:,}") # 1,000,000
print(f"{large_num:_.2f}") # 1_000_000.00
F-strings excel at formatting different number types and bases:
number = 42
# Different number bases
print(f"{number:b}") # 101010 (binary)
print(f"{number:o}") # 52 (octal)
print(f"{number:x}") # 2a (hexadecimal lowercase)
print(f"{number:X}") # 2A (hexadecimal uppercase)
print(f"{number:#x}") # 0x2a (hexadecimal with prefix)
# Percentage formatting
ratio = 0.8547
print(f"{ratio:.2%}") # 85.47%
# Scientific notation
big = 12345678
print(f"{big:e}") # 1.234568e+07
print(f"{big:.2e}") # 1.23e+07
Date and datetime objects work seamlessly with f-strings:
from datetime import datetime
now = datetime.now()
print(f"{now:%Y-%m-%d}") # 2024-01-15
print(f"{now:%B %d, %Y}") # January 15, 2024
print(f"{now:%H:%M:%S}") # 14:30:45
print(f"{now:%Y-%m-%d %H:%M:%S}") # 2024-01-15 14:30:45
F-strings support debugging with the = specifier (Python 3.8+):
x = 10
y = 20
# Debug output shows both expression and value
print(f"{x=}, {y=}, {x+y=}")
# Output: x=10, y=20, x+y=30
# Combines with format specs
price = 19.99
print(f"{price=:.0f}")
# Output: price=20
str.format(): Flexibility for Complex Scenarios
The format() method uses positional and keyword arguments with {} placeholders. This approach works in Python 2.7+ and offers advantages for template reuse.
# Positional arguments
template = "Hello, {}! You are {} years old."
print(template.format("Alice", 30))
# Output: Hello, Alice! You are 30 years old.
# Indexed positional arguments
message = "{0} {1} {0}".format("hello", "world")
# Output: hello world hello
# Keyword arguments
user_template = "Name: {name}, Age: {age}, City: {city}"
print(user_template.format(name="Bob", age=25, city="NYC"))
# Output: Name: Bob, Age: 25, City: NYC
The format() method shines when working with dictionaries and attribute access:
# Dictionary unpacking
user = {"name": "Charlie", "age": 35, "role": "Developer"}
print("User {name} ({age}) - {role}".format(**user))
# Output: User Charlie (35) - Developer
# Accessing dictionary keys
data = {"server": "prod-01", "status": "active"}
print("Server: {0[server]}, Status: {0[status]}".format(data))
# Output: Server: prod-01, Status: active
# Accessing object attributes
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Diana", 28)
print("Person: {0.name}, Age: {0.age}".format(person))
# Output: Person: Diana, Age: 28
Advanced formatting with format():
# Nested formatting
for align, text in [('<', 'left'), ('^', 'center'), ('>', 'right')]:
print("{:{align}20}".format(text, align=align))
# Output:
# left
# center
# right
# Dynamic width and precision
width = 10
precision = 3
value = 3.14159
print("{:>{width}.{precision}f}".format(value, width=width, precision=precision))
# Output: 3.142
# Type conversion with !r, !s, !a
text = "hello\nworld"
print("{!s}".format(text)) # hello\nworld (str())
print("{!r}".format(text)) # 'hello\nworld' (repr())
Old-Style % Formatting: Legacy and Logging
The % operator provides C-style string formatting. While older, it remains prevalent in logging configurations and legacy codebases.
# Basic usage
name = "Eve"
age = 32
print("Name: %s, Age: %d" % (name, age))
# Output: Name: Eve, Age: 32
# Single value (no tuple needed)
print("Hello, %s!" % "Frank")
# Output: Hello, Frank!
# Named placeholders with dictionary
print("%(name)s is %(age)d years old" % {"name": "Grace", "age": 27})
# Output: Grace is 27 years old
Format specifiers with % formatting:
# Integer formatting
print("%d" % 42) # 42
print("%5d" % 42) # " 42" (width 5, right-aligned)
print("%05d" % 42) # "00042" (zero-padded)
print("%-5d" % 42) # "42 " (left-aligned)
# Float formatting
pi = 3.14159
print("%.2f" % pi) # 3.14
print("%8.2f" % pi) # " 3.14" (width 8, 2 decimals)
print("%e" % 1000000) # 1.000000e+06
# Hex, octal, binary
print("%x" % 255) # ff
print("%X" % 255) # FF
print("%o" % 8) # 10
The % operator works well with logging because format strings are only evaluated when needed:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Lazy evaluation - format string only processed if logged
expensive_value = "computed_result"
logger.debug("Debug info: %s", expensive_value) # Not evaluated if debug disabled
logger.info("Processing %s with %d items", "dataset", 1000)
Performance Comparison
F-strings generally outperform other methods:
import timeit
name = "Test"
number = 42
# F-string
t1 = timeit.timeit(lambda: f"{name}: {number}", number=1000000)
# format()
t2 = timeit.timeit(lambda: "{}: {}".format(name, number), number=1000000)
# % formatting
t3 = timeit.timeit(lambda: "%s: %d" % (name, number), number=1000000)
print(f"F-string: {t1:.4f}s")
print(f"format(): {t2:.4f}s")
print(f"% format: {t3:.4f}s")
# Typical results show f-strings ~30-40% faster
Choosing the Right Method
Use f-strings for:
- New Python 3.6+ code
- Maximum performance and readability
- Quick debugging with
=specifier - Direct variable/expression interpolation
Use str.format() for:
- Template reuse across multiple calls
- Python 2.7-3.5 compatibility
- Complex nested formatting
- Dictionary/object attribute access
Use % formatting for:
- Logging statements (lazy evaluation)
- Maintaining legacy code
- C-style format string familiarity
Modern Python development should default to f-strings unless specific requirements dictate otherwise. The performance benefits, readability improvements, and concise syntax make f-strings the pragmatic choice for most formatting tasks.