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.

Liked this? There's more.

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