Python String Formatting & F-Strings: The Complete Guide for 2026

February 13, 2026 · 18 min read

Python has evolved through three eras of string formatting: the % operator inherited from C, the str.format() method introduced in Python 2.6, and f-strings (formatted string literals) added in Python 3.6. Each generation improved readability and power. Today, f-strings are the standard, and Python 3.12 made them even more flexible. This guide covers every formatting technique you will need, from basic interpolation through the format specification mini-language, number formatting, date/time, debugging tricks, and real-world patterns.

1. The Evolution of String Formatting

Understanding all three methods matters because you will encounter legacy code using % and .format(), and certain edge cases still favor them.

% Formatting (printf-style)

The oldest method, borrowed from C's printf. Available since Python 1.x:

# Basic substitution
name = "Alice"
age = 30
print("Hello, %s! You are %d years old." % (name, age))
# Hello, Alice! You are 30 years old.

# Named placeholders with a dictionary
print("%(name)s is %(age)d" % {"name": "Bob", "age": 25})

# Common type codes: %s (string), %d (integer), %f (float), %x (hex)
print("Pi is approximately %.4f" % 3.14159)   # Pi is approximately 3.1416
print("Hex: %x, Oct: %o" % (255, 255))        # Hex: ff, Oct: 377

Downsides: Tuples break with single-element ("%s" % (value,) needs trailing comma), limited expression support, verbose with many variables.

str.format() Method

Introduced in Python 2.6 / 3.0 (PEP 3101). More readable and flexible than %:

# Positional arguments
print("Hello, {}! You are {} years old.".format("Alice", 30))

# Numbered arguments (reusable)
print("{0} scored {1}. {0} wins!".format("Alice", 95))

# Named arguments
print("{name} is {age} years old".format(name="Alice", age=30))

# Accessing attributes and items
point = (3, 7)
print("x={0[0]}, y={0[1]}".format(point))

# Object attributes
import datetime
d = datetime.date(2026, 2, 13)
print("{:%B %d, %Y}".format(d))  # February 13, 2026

F-Strings (Python 3.6+, PEP 498)

The modern standard. Prefix a string with f and embed expressions directly in braces:

name = "Alice"
age = 30
print(f"Hello, {name}! You are {age} years old.")

# Any expression works inside the braces
print(f"Next year you'll be {age + 1}")
print(f"Name uppercase: {name.upper()}")
print(f"Is adult: {age >= 18}")
print(f"Items: {', '.join(['a', 'b', 'c'])}")

2. F-String Fundamentals

F-strings evaluate expressions at runtime and embed the result into the string. They support anything that produces a value:

# Variables and expressions
x, y = 10, 20
print(f"Sum: {x + y}, Product: {x * y}")

# Method calls
text = "hello world"
print(f"Title: {text.title()}")        # Title: Hello World
print(f"Words: {len(text.split())}")   # Words: 2

# Attribute access
import math
print(f"Pi: {math.pi:.6f}")           # Pi: 3.141593

# Ternary expressions
score = 85
print(f"Grade: {'Pass' if score >= 60 else 'Fail'}")

# Function calls
def greet(n):
    return f"Hi, {n}!"

print(f"Message: {greet('Bob')}")      # Message: Hi, Bob!

# List/dict access
users = ["Alice", "Bob", "Charlie"]
config = {"host": "localhost", "port": 8080}
print(f"First user: {users[0]}")
print(f"Host: {config['host']}:{config['port']}")

3. The Format Specification Mini-Language

The format spec follows the colon inside braces: f"{value:spec}". The full syntax is:

# Syntax: [[fill]align][sign][z][#][0][width][grouping][.precision][type]

# ALIGNMENT: < (left), > (right), ^ (center)
name = "Python"
print(f"|{name:<20}|")    # |Python              |
print(f"|{name:>20}|")    # |              Python|
print(f"|{name:^20}|")    # |       Python       |

# FILL CHARACTER (any character before the alignment)
print(f"|{name:*<20}|")   # |Python**************|
print(f"|{name:*>20}|")   # |**************Python|
print(f"|{name:-^20}|")   # |-------Python-------|
print(f"|{name:.^20}|")   # |.......Python.......|

# WIDTH (minimum field width)
for item, price in [("Coffee", 4.5), ("Sandwich", 8.99), ("Cake", 12)]:
    print(f"{item:<12} ${price:>6.2f}")
# Coffee       $  4.50
# Sandwich     $  8.99
# Cake         $ 12.00

# SIGN: + (always show), - (default, negative only), space (space for positive)
num = 42
print(f"{num:+d}")    # +42
print(f"{-num:+d}")   # -42
print(f"{num: d}")    #  42  (space before positive numbers)

Type Codes

# INTEGER types
n = 255
print(f"Decimal:  {n:d}")     # 255
print(f"Binary:   {n:b}")     # 11111111
print(f"Octal:    {n:o}")     # 377
print(f"Hex (lc): {n:x}")     # ff
print(f"Hex (UC): {n:X}")     # FF

# With prefix using #
print(f"Binary:   {n:#b}")    # 0b11111111
print(f"Octal:    {n:#o}")    # 0o377
print(f"Hex:      {n:#x}")    # 0xff

# FLOAT types
pi = 3.14159265
print(f"Fixed:      {pi:f}")      # 3.141593 (default 6 decimals)
print(f"2 decimals: {pi:.2f}")    # 3.14
print(f"Scientific: {pi:e}")      # 3.141593e+00
print(f"Sci 2 dec:  {pi:.2e}")    # 3.14e+00
print(f"General:    {pi:g}")      # 3.14159 (removes trailing zeros)
print(f"Percentage: {0.856:%}")   # 85.600000%
print(f"Pct 1 dec:  {0.856:.1%}") # 85.6%

# STRING type
print(f"{'Hello':s}")          # Hello (default for strings)
print(f"{'Hello':.3s}")        # Hel (truncate to 3 chars)

4. Number Formatting

Formatting numbers is one of the most common f-string tasks. Here is every pattern you need:

# THOUSANDS SEPARATORS
big = 1234567890
print(f"{big:,}")              # 1,234,567,890
print(f"{big:_}")              # 1_234_567_890

# CURRENCY
price = 1299.5
print(f"${price:,.2f}")        # $1,299.50
print(f"EUR {price:,.2f}")     # EUR 1,299.50

# PADDING WITH ZEROS
order_id = 42
print(f"ORD-{order_id:06d}")   # ORD-000042
print(f"v{3:03d}.{8:03d}")     # v003.008

# PERCENTAGE
ratio = 0.8567
print(f"{ratio:.1%}")          # 85.7%
print(f"{ratio:.0%}")          # 86%

# FIXED POINT precision
value = 3.14159
print(f"{value:.2f}")          # 3.14
print(f"{value:.4f}")          # 3.1416
print(f"{value:.0f}")          # 3

# SCIENTIFIC NOTATION
large = 6.022e23
print(f"{large:.3e}")          # 6.022e+23
print(f"{large:.3E}")          # 6.022E+23

# BINARY / HEX for bytes and colors
r, g, b = 255, 128, 0
print(f"#{r:02x}{g:02x}{b:02x}")   # #ff8000
print(f"Permissions: {0o755:b}")     # 111101101

# SIGNIFICANT FIGURES with g
print(f"{0.00012345:.3g}")     # 0.000123
print(f"{12345.6789:.5g}")     # 12346
print(f"{100.0:.3g}")          # 100

5. Date and Time Formatting

F-strings work directly with datetime objects using strftime codes after the colon:

from datetime import datetime, date, timedelta

now = datetime(2026, 2, 13, 14, 30, 45)

# Common date formats
print(f"{now:%Y-%m-%d}")            # 2026-02-13
print(f"{now:%d/%m/%Y}")            # 13/02/2026
print(f"{now:%B %d, %Y}")           # February 13, 2026
print(f"{now:%b %d, %Y}")           # Feb 13, 2026

# Time formats
print(f"{now:%H:%M:%S}")            # 14:30:45
print(f"{now:%I:%M %p}")            # 02:30 PM

# Combined date and time
print(f"{now:%Y-%m-%d %H:%M}")      # 2026-02-13 14:30
print(f"{now:%A, %B %d at %I:%M %p}")  # Friday, February 13 at 02:30 PM

# ISO format
print(f"{now:%Y-%m-%dT%H:%M:%S}")   # 2026-02-13T14:30:45

# Day of week, week number
print(f"{now:%A}")                   # Friday
print(f"Week {now:%U} of {now:%Y}") # Week 06 of 2026

# Logging timestamps
print(f"[{now:%Y-%m-%d %H:%M:%S}] Server started")

# Relative dates
deadline = now + timedelta(days=7)
print(f"Due by {deadline:%b %d}")    # Due by Feb 20

6. Debugging with f-strings: The = Specifier

Python 3.8 introduced the = specifier, which prints both the expression and its value. This is one of the most useful debugging features in Python:

# Basic variable debugging
x = 42
name = "Alice"
print(f"{x=}")           # x=42
print(f"{name=}")        # name='Alice'

# Expression debugging
items = [1, 2, 3, 4, 5]
print(f"{len(items)=}")          # len(items)=5
print(f"{sum(items)=}")          # sum(items)=15
print(f"{items[0]=}")            # items[0]=1

# With format specs
price = 19.99
print(f"{price=:.2f}")           # price=19.99
print(f"{price * 1.1=:.2f}")    # price * 1.1=21.99

# Spacing around the =
print(f"{x = }")         # x = 42  (preserves spaces)
print(f"{name = !r}")    # name = 'Alice'

# Complex expressions
data = {"users": 150, "active": 89}
print(f"{data['active'] / data['users']:.1%=}")
# data['active'] / data['users']:.1%=59.3%

# Great for quick debugging loops
for i in range(3):
    print(f"{i=}, {i**2=}, {i**3=}")
# i=0, i**2=0, i**3=0
# i=1, i**2=1, i**3=1
# i=2, i**2=4, i**3=8

# Conversion flags with =
value = "hello\nworld"
print(f"{value=}")       # value='hello\nworld'
print(f"{value=!r}")     # value='hello\nworld'  (repr)
print(f"{value=!s}")     # value=hello\nworld     (str)
print(f"{value=!a}")     # value='hello\nworld'  (ascii)

7. Multiline and Raw F-Strings

# MULTILINE with triple quotes
user = {"name": "Alice", "role": "Admin", "email": "alice@dev.io"}
message = f"""
User Report
-----------
Name:  {user['name']}
Role:  {user['role']}
Email: {user['email']}
"""
print(message)

# IMPLICIT CONCATENATION (keeps lines short, no extra newlines)
greeting = (
    f"Hello {user['name']}, "
    f"you are logged in as {user['role']}. "
    f"Contact: {user['email']}"
)

# RAW F-STRINGS (rf or fr prefix) -- no escape processing
path = "Documents"
print(rf"C:\Users\{name}\{path}")   # C:\Users\Alice\Documents
# Without r: backslashes would be escape sequences

# FR also works (case insensitive prefix)
print(FR"Path: C:\{path}\data")

# Multiline raw f-string
regex_info = rf"""
Pattern: \d+\.\d+
Matches: {42.5}
Flags:   re.MULTILINE
"""

8. Python 3.12+ F-String Improvements (PEP 701)

Python 3.12 removed several long-standing f-string limitations, making them truly first-class expressions:

# 1. REUSE THE SAME QUOTE TYPE (new in 3.12)
# Before 3.12, you had to alternate quote types:
user = {"name": "Alice"}
# Old way: f"Name: {user['name']}"   (single inside double)
# New way (3.12+):
print(f"Name: {user["name"]}")       # Same quotes, now legal!

# 2. NESTED F-STRINGS with same quotes
songs = ["Yesterday", "Imagine", "Bohemian Rhapsody"]
print(f"Favorite: {f"{songs[0]}" if songs else "None"}")

# 3. BACKSLASHES inside f-string expressions
# Before 3.12: SyntaxError
# Now valid:
items = ["a", "b", "c"]
print(f"Joined: {'\n'.join(items)}")
# Joined: a
# b
# c

newline = "first\nsecond"
print(f"Lines: {newline.split('\n')}")

# 4. COMMENTS in multiline f-string expressions (3.12+)
result = f"{
    x    # the x value
    + y  # plus y
}"

# 5. UNLIMITED NESTING DEPTH
# Python 3.12 removed the arbitrary nesting limit
print(f"{'='*20}")  # Always worked
print(f"{f"{f"{42}"}"}")  # Deeply nested -- now valid

9. Performance Comparison

F-strings are the fastest formatting method. Here is a benchmark comparison:

import timeit

name = "Alice"
age = 30
n = 1_000_000

# F-string
t1 = timeit.timeit(lambda: f"{name} is {age} years old", number=n)

# str.format()
t2 = timeit.timeit(lambda: "{} is {} years old".format(name, age), number=n)

# % formatting
t3 = timeit.timeit(lambda: "%s is %d years old" % (name, age), number=n)

# Concatenation
t4 = timeit.timeit(lambda: name + " is " + str(age) + " years old", number=n)

# Typical results (Python 3.12, relative to f-string = 1.0x):
# f-string:        1.0x  (fastest)
# % formatting:    1.2x
# str.format():    1.5x
# concatenation:   1.3x
Why are f-strings faster? The Python compiler converts f-strings into optimized bytecode at compile time. The expression f"{name} is {age}" compiles to a series of LOAD and FORMAT_VALUE instructions, avoiding the overhead of a method call (.format()) or tuple creation (%). For hot loops and high-throughput code, f-strings measurably reduce CPU time.

10. Common Real-World Patterns

Table Output

headers = ["Name", "Role", "Salary"]
rows = [
    ("Alice", "Engineer", 95000),
    ("Bob", "Designer", 82000),
    ("Charlie", "Manager", 110000),
]

# Header
print(f"{'Name':<12} {'Role':<12} {'Salary':>10}")
print("-" * 36)
for name, role, salary in rows:
    print(f"{name:<12} {role:<12} ${salary:>9,}")

# Output:
# Name         Role           Salary
# ------------------------------------
# Alice        Engineer       $  95,000
# Bob          Designer       $  82,000
# Charlie      Manager        $ 110,000

File Sizes (Human-Readable)

def format_size(size_bytes: int) -> str:
    """Convert bytes to human-readable file size."""
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if size_bytes < 1024:
            return f"{size_bytes:.1f} {unit}"
        size_bytes /= 1024
    return f"{size_bytes:.1f} PB"

print(format_size(1536))         # 1.5 KB
print(format_size(2_500_000))    # 2.4 MB
print(format_size(5_000_000_000))  # 4.7 GB

Progress Bars

def progress_bar(current: int, total: int, width: int = 30) -> str:
    pct = current / total
    filled = int(width * pct)
    bar = "#" * filled + "-" * (width - filled)
    return f"\r[{bar}] {pct:.0%} ({current}/{total})"

# Usage in a loop
import time
for i in range(101):
    print(progress_bar(i, 100), end="", flush=True)
    time.sleep(0.02)
# [##############################] 100% (100/100)

Padding and Alignment

# Menu / receipt layout
items = [("Latte", 4.50), ("Croissant", 3.25), ("Muffin", 2.99)]
print("=" * 30)
for item, price in items:
    print(f"  {item:. <22} ${price:.2f}")
total = sum(p for _, p in items)
print("-" * 30)
print(f"  {'TOTAL':. <22} ${total:.2f}")

# Hex dump
data = b"\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64"
for i in range(0, len(data), 8):
    chunk = data[i:i+8]
    hex_part = " ".join(f"{b:02x}" for b in chunk)
    ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
    print(f"{i:08x}  {hex_part:<24} |{ascii_part}|")
# 00000000  48 65 6c 6c 6f 20 57 6f  |Hello Wo|
# 00000008  72 6c 64                  |rld|

Currency Formatting

amount = 1234567.891

# US Dollar
print(f"${amount:,.2f}")           # $1,234,567.89

# Negative amounts
loss = -4500.00
print(f"{loss:+,.2f}")             # -4,500.00
print(f"({abs(loss):,.2f})")       # (4,500.00) -- accounting style

# Padded for reports
for label, val in [("Revenue", 50000), ("Expenses", -32500), ("Profit", 17500)]:
    print(f"  {label:<12} {'$':>1}{val:>12,.2f}")

11. Template Strings: When F-Strings Are Not Enough

The string.Template class is the safe choice when formatting user-supplied template strings. Unlike f-strings, Template strings cannot execute arbitrary code:

from string import Template

# User-supplied template -- safe because it cannot call functions
template = Template("Hello, $name! You have $count messages.")
result = template.substitute(name="Alice", count=5)
print(result)  # Hello, Alice! You have 5 messages.

# safe_substitute ignores missing keys instead of raising
t = Template("$greeting, $name!")
print(t.safe_substitute(greeting="Hi"))
# Hi, $name!  (missing key left as-is)

# WHEN TO USE TEMPLATE vs F-STRING:
# - F-string: developer-controlled format strings (trusted code)
# - Template: user-supplied templates (untrusted input)
# - .format(): NEVER with user input! .format() can leak data:
#   "{0.__class__.__mro__}".format(obj)  # Exposes internals!

# Template supports ${var} for adjacent text
t = Template("File: ${name}_backup.tar.gz")
print(t.substitute(name="database"))  # File: database_backup.tar.gz

12. Best Practices and Common Gotchas

# GOTCHA 1: Braces in f-strings -- double them to escape
print(f"Set notation: {{{1, 2, 3}}}")   # Set notation: {1, 2, 3}
print(f"JSON: {{\"key\": \"{name}\"}}")  # JSON: {"key": "Alice"}

# GOTCHA 2: Backslashes before Python 3.12
# In Python < 3.12, you cannot use backslashes inside f-string expressions:
# print(f"{'\\n'.join(items)}")  # SyntaxError in < 3.12
# Workaround: assign to a variable first
nl = "\n"
print(f"{nl.join(items)}")  # Works in all versions

# GOTCHA 3: Single-element tuple with %
value = "test"
# print("Value: %s" % value)     # Works
# print("Value: %s" % (value))   # Works BUT risky if value is a tuple
# print("Value: %s" % (value,))  # Always safe -- trailing comma

# BEST PRACTICE 1: Prefer f-strings for all new code
# Bad:  "Hello, %s" % name
# Bad:  "Hello, {}".format(name)
# Good: f"Hello, {name}"

# BEST PRACTICE 2: Keep expressions simple
# Bad:  f"{db.query(User).filter(active=True).order_by('name').first().name}"
# Good:
user = db.query(User).filter(active=True).order_by("name").first()
print(f"User: {user.name}")

# BEST PRACTICE 3: Use !r for debugging strings (shows quotes and escapes)
path = "/tmp/my file.txt"
print(f"Loading {path!r}")     # Loading '/tmp/my file.txt'

# BEST PRACTICE 4: Use conversion flags appropriately
# !s  -- calls str() on the value (default)
# !r  -- calls repr() on the value (debugging)
# !a  -- calls ascii() on the value (escapes non-ASCII)
emoji = "Hello \U0001f600"
print(f"{emoji!a}")  # 'Hello \U0001f600'

# BEST PRACTICE 5: Format spec for consistent output
# Always specify .2f for money, leading zeros for IDs
print(f"Price: ${19.9:.2f}")       # Price: $19.90
print(f"Order #{42:08d}")          # Order #00000042

Frequently Asked Questions

What are Python f-strings and when were they introduced?

F-strings (formatted string literals) were introduced in Python 3.6 via PEP 498. They let you embed expressions directly inside string literals by prefixing the string with f or F and placing expressions inside curly braces. For example, f"Hello, {name}!" evaluates the variable name and inserts the result. F-strings are the recommended way to format strings in modern Python because they are readable, concise, and faster than both % formatting and str.format().

How do I format numbers with commas in Python?

Use the comma format specifier inside an f-string: f"{1234567:,}" produces 1,234,567. For floats with commas and fixed decimal places, use f"{amount:,.2f}" which gives 1,234,567.89. You can also use underscores with f"{value:_}" for 1_234_567, which is common in code for readability. These format specifiers work identically in f-strings, str.format(), and the built-in format() function.

What does f'{variable=}' do in Python?

The = specifier, introduced in Python 3.8, is a debugging feature that prints both the expression text and its value. Writing f"{x=}" produces the string x=42 if x is 42. It works with any expression: f"{len(items)=}" outputs len(items)=5. You can combine it with format specs like f"{price=:.2f}" to get price=19.99. This eliminates the tedious pattern of writing f"x={x}" manually.

Are f-strings faster than .format() and % formatting?

Yes, f-strings are the fastest string formatting method in Python. In benchmarks, f-strings are typically 30-60% faster than str.format() and 10-30% faster than % formatting. This is because f-strings are evaluated at compile time and converted to efficient bytecode, while .format() requires a method call and argument parsing at runtime. For hot loops and high-throughput code, f-strings measurably reduce CPU time.

How do I use multiline f-strings in Python?

Use triple quotes for multiline f-strings: f"""...""" or f'''...'''. Each line can contain expressions in curly braces. You can also use parentheses to break a single f-string across lines without triple quotes: message = (f"Hello {name}, " f"you have {count} " f"notifications."). Adjacent f-strings are concatenated automatically by Python, which keeps each line short and readable.

Can I use f-strings with dictionaries and complex expressions?

Yes, f-strings support any valid Python expression including dictionary access, method calls, list comprehensions, ternary operators, and function calls. For dictionaries, use different quote types inside and outside: f"{user['name']}" inside double-quoted f-string. In Python 3.12+, you can reuse the same quote style thanks to PEP 701: f"{user["name"]}" is now valid. Complex expressions like f"{', '.join(items)}" and ternary operators also work.

Conclusion

F-strings are the clear winner for string formatting in modern Python. They are faster, more readable, and more concise than % formatting and str.format(). The format specification mini-language gives you precise control over alignment, padding, number formatting, and type conversion. Python 3.8 added the invaluable = debugging specifier, and Python 3.12 removed the last remaining limitations around quotes, backslashes, and nesting. Use string.Template only when handling user-supplied format strings, and always prefer f-strings for everything else.

⚙ Keep learning: Explore Python Beginners Guide for fundamentals, Python Type Hints for typing your formatted output, and Python Logging for formatting log messages correctly.

Related Resources

Related Resources

Python Beginners Guide
Fundamentals, syntax, and getting started with Python
Python Type Hints Guide
Typing your functions and data structures
Python Logging Guide
Formatting log messages with the logging module
Python Dataclasses Guide
__repr__ and __str__ with formatted output
Python Data Structures
Formatting lists, dicts, sets, and tuples
Python Decorators Guide
Advanced function patterns, wrapping, and composition