Python f-strings — Modern String Formatting Mastery

Python Basicsf-stringsFree Lesson

Advertisement

Learning Objectives

By the end of this tutorial, you will be able to:

  • Understand what f-strings are and why they are the preferred string formatting method in Python
  • Use basic f-string syntax with variables and expressions
  • Apply format specifiers for width, precision, alignment, fill characters, and types
  • Use the debugging = syntax (Python 3.8+) to inspect values
  • Create multiline and nested f-strings
  • Compare f-strings with %-formatting, .format(), and string.Template
  • Identify common f-string mistakes and how to avoid them

What Are f-strings

f-strings (formatted string literals) were introduced in Python 3.6 (PEP 498). They provide a concise and readable way to embed expressions inside string literals. The f or F prefix tells Python to evaluate the expression inside the braces at runtime and include the result in the string.

name = "Alice"
greeting = f"Hello, {name}!"
print(greeting)  # Hello, Alice!

f-strings are faster than both %-formatting and .format() because the expression is evaluated at runtime and the result is directly embedded into the string, avoiding function call overhead.

Basic Syntax

Variable Interpolation

first_name = "John"
last_name = "Doe"
age = 30

print(f"Name: {first_name} {last_name}, Age: {age}")
# Output: Name: John Doe, Age: 30

Using Dict and List Access

person = {"name": "Sara", "city": "Paris"}
scores = [95, 87, 92]

print(f"City: {person['city']}")   # Output: City: Paris
print(f"First score: {scores[0]}") # Output: First score: 95

Calling Methods Inside f-strings

message = "hello world"
print(f"Uppercase: {message.upper()}")  # Output: Uppercase: HELLO WORLD
print(f"Title: {message.title()}")      # Output: Title: Hello World

Using Format Specifiers Inline

pi = 3.14159265
print(f"Pi to 2 decimals: {pi:.2f}")  # Output: Pi to 2 decimals: 3.14

Expressions in f-strings

f-strings support any valid Python expression inside the braces.

Arithmetic Expressions

x = 10
y = 3

print(f"{x} + {y} = {x + y}")   # Output: 10 + 3 = 13
print(f"{x} * {y} = {x * y}")   # Output: 10 * 3 = 30
print(f"Power: {x ** y}")        # Output: Power: 1000

Conditional Expressions

score = 85
print(f"Result: {'Pass' if score >= 60 else 'Fail'}")
# Output: Result: Pass

print(f"Status: {'Excellent' if score >= 90 else 'Good' if score >= 75 else 'Needs improvement'}")
# Output: Status: Good

Calling Functions

import math

radius = 5
print(f"Area of circle: {math.pi * radius ** 2:.2f}")  # Output: Area of circle: 78.54

words = ["python", "is", "awesome"]
print(f"Joined: {' '.join(words)}")  # Output: Joined: python is awesome

Using Comprehensions

numbers = [1, 2, 3, 4, 5]
print(f"Squares: {[n ** 2 for n in numbers]}")
# Output: Squares: [1, 4, 9, 16, 25]

print(f"Even numbers: {[n for n in numbers if n % 2 == 0]}")
# Output: Even numbers: [2, 4]

Format Specifier Mini-Language

The format specifier inside f-strings follows Python's format specification mini-language:

[[fill]align][sign][z][#][0][width][grouping_option][.precision][type]

Width and Alignment

name = "Alice"
print(f"|{name:10}|")    # Right-aligned (default): |Alice     |
print(f"|{name:<10}|")   # Left-aligned:            |Alice     |
print(f"|{name:>10}|")   # Right-aligned:           |     Alice|
print(f"|{name:^10}|")   # Center-aligned:          |  Alice   |
print(f"|{name:*^10}|")  # Center with * fill:      |**Alice***|

Precision

pi = 3.141592653589793

print(f"2 decimals: {pi:.2f}")   # Output: 2 decimals: 3.14
print(f"4 decimals: {pi:.4f}")   # Output: 4 decimals: 3.1416
print(f"6 decimals: {pi:.6f}")   # Output: 6 decimals: 3.141593

long_string = "This is a very long string"
print(f"Truncated: {long_string:.10}")  # Output: Truncated: This is a

Fill Characters and Sign

price = 49.99
print(f"Price: ${price:0>8.2f}")   # Output: Price: $00049.99
print(f"Price: ${price:.<8.2f}")   # Output: Price: $49.99...

positive = 42
negative = -42
print(f"With +: {positive:+5d}, {negative:+5d}")  # Output: With +:   +42,   -42

Numeric Types

print(f"Binary: {42:b}")       # Output: Binary: 101010
print(f"Octal: {42:o}")        # Output: Octal: 52
print(f"Hex: {42:x}")          # Output: Hex: 2a
print(f"HEX: {42:X}")          # Output: HEX: 2A
print(f"Scientific: {123456.789:.2e}")  # Output: Scientific: 1.23e+05

ratio = 0.8567
print(f"Percent: {ratio:.1%}")  # Output: Percent: 85.7%

Grouping

population = 7_900_000_000
print(f"World population: {population:,}")
# Output: World population: 7,900,000,000

price = 1234567.89
print(f"Price: ${price:,.2f}")
# Output: Price: $1,234,567.89

Debugging with = (Python 3.8+)

Python 3.8 introduced the = specifier in f-strings, which automatically prints the expression and its value. This is invaluable for debugging.

x = 10
y = 20
print(f"{x + y = }")
# Output: x + y = 30

name = "Alice"
print(f"{name = }")
# Output: name = 'Alice'

import math
print(f"{math.pi = }")
# Output: math.pi = 3.141592653589793

# With format specifier
print(f"{math.pi = :.2f}")
# Output: math.pi = 3.14

# Debugging in complex expressions
result = {"key": "value", "count": 42}
print(f"{result.get('count') = }")
# Output: result.get('count') = 42

Multiline f-strings

f-strings work with triple quotes, allowing you to create multiline formatted strings.

name = "Alice"
age = 30
city = "Paris"

# Triple-quoted f-string
message = f"""Name: {name}
Age: {age}
City: {city}"""

print(message)
# Output:
# Name: Alice
# Age: 30
# City: Paris

# Line continuation with backslash
formatted = (
    f"Name: {name}, "
    f"Age: {age}, "
    f"City: {city}"
)
print(formatted)
# Output: Name: Alice, Age: 30, City: Paris

Nested f-strings

You can nest f-strings inside other f-strings. Inner f-strings are evaluated first.

width = 10
text = "Hello"

# Nested f-string with alignment
print(f"|{f'{text}':^{width}}|")   # Output: |  Hello   |

# Nested with format specifiers
value = 42
print(f"|{f'{value:.2f}':>10}|")   # Output: |     42.00|

# Building a formatted table
data = [
    ("Alice", 95.5),
    ("Bob", 87.3),
    ("Charlie", 92.1),
]

header = f"|{'Name':^10}|{'Score':^10}|"
separator = f"|{'-'*10}|{'-'*10}|"
print(header)
print(separator)
for name, score in data:
    row = f"|{f'{name}':^10}|{f'{score:.1f}':^10}|"
    print(row)
# Output:
# |   Name   |  Score   |
# |----------|----------|
# |  Alice   |   95.5   |
# |   Bob    |   87.3   |
# | Charlie  |   92.1   |

# Using nested f-strings for conditional formatting
status = "active"
label = f"Status: {f'ACTIVE' if status == 'active' else 'INACTIVE'}"
print(label)
# Output: Status: ACTIVE

f-strings vs Other Methods

f-strings vs %-formatting

name = "Alice"
age = 30

# %-formatting (old style)
old_style = "Name: %s, Age: %d" % (name, age)

# f-string
new_style = f"Name: {name}, Age: {age}"

# Both output: Name: Alice, Age: 30
# f-string is more readable and doesn't require remembering type codes

f-strings vs .format()

name = "Alice"
age = 30

# .format() method
formatted = "Name: {}, Age: {}".format(name, age)

# .format() with named placeholders
formatted_named = "Name: {name}, Age: {age}".format(name=name, age=age)

# f-string
f_formatted = f"Name: {name}, Age: {age}"

# All output: Name: Alice, Age: 30
# f-string is more concise and easier to read

f-strings vs string.Template

from string import Template

name = "Alice"
age = 30

# Template
template = Template("Name: $name, Age: $age")
template_formatted = template.substitute(name=name, age=age)

# f-string
f_formatted = f"Name: {name}, Age: {age}"

# Both output: Name: Alice, Age: 30
# Template is safer for user-provided templates but more verbose

Performance Comparison

f-strings are typically the fastest string formatting method in Python.

import timeit

name = "Alice"
age = 30
city = "Paris"

# % formatting
def percent_format():
    return "Name: %s, Age: %d, City: %s" % (name, age, city)

# .format() method
def format_method():
    return "Name: {}, Age: {}, City: {}".format(name, age, city)

# f-string
def fstring_format():
    return f"Name: {name}, Age: {age}, City: {city}"

# Benchmark each method
iterations = 1000000

percent_time = timeit.timeit(percent_format, number=iterations)
format_time = timeit.timeit(format_method, number=iterations)
fstring_time = timeit.timeit(fstring_format, number=iterations)

print(f"% formatting:      {percent_time:.4f}s")
print(f".format() method:  {format_time:.4f}s")
print(f"f-string:          {fstring_time:.4f}s")
# Typical output (varies by system):
# % formatting:      0.1200s
# .format() method:  0.1800s
# f-string:          0.0800s

f-strings are generally 2-3x faster than .format() and 20-30% faster than % formatting because they are evaluated at compile time where possible and avoid function call overhead.

Common Patterns

JSON-style Formatting

import json

user = {
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com"
}

print(json.dumps(user, indent=2))

# f-string for quick key-value formatting
print(f"User: {user['name']} ({user['age']})")
# Output: User: Alice (30)

for key, value in user.items():
    print(f"  {key}: {value}")
# Output:
#   name: Alice
#   age: 30
#   email: alice@example.com

SQL Query Building

table = "users"
columns = "id, name, email"
where_clause = "age > 25"

query = f"SELECT {columns} FROM {table} WHERE {where_clause}"
print(query)
# Output: SELECT id, name, email FROM users WHERE age > 25

# For production code, always use parameterized queries:
# cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

Logging and Debug Messages

import logging

user_name = "Alice"
action = "login"
status = "success"

# f-string in logging (recommended for performance)
logging.info(f"User {user_name} performed {action}: {status}")

# Error messages with context
def process_payment(amount, currency):
    if amount <= 0:
        raise ValueError(f"Invalid payment amount: {amount} {currency}")
    return f"Processed {currency} {amount:.2f}"

try:
    process_payment(-50, "USD")
except ValueError as e:
    print(f"Error: {e}")
    # Output: Error: Invalid payment amount: -50 USD

Date and Time Formatting

from datetime import datetime

now = datetime.now()

print(f"Current date: {now:%Y-%m-%d}")
print(f"Current time: {now:%H:%M:%S}")
print(f"Full datetime: {now:%Y-%m-%d %H:%M:%S}")
print(f"Date: {now:%B %d, %Y}")

Common Mistakes

1. Confusing f-string Braces with Dict or Set Literals

# Wrong: Python thinks these are format specifiers
# print(f"{'key': 'value'}")  # SyntaxError

# Solution: use variables for complex expressions
pair = {"key": "value"}
print(f"Dict: {pair}")
# Output: Dict: {'key': 'value'}

2. Using Backslashes Inside f-string Expressions

# Wrong: Backslashes not allowed inside f-string expressions (Python < 3.12)
# print(f"Newline: {'\n'}")  # SyntaxError in Python < 3.12

# Solution: Use variables or chr()
newline = "\n"
print(f"Newline:{newline}After")

# Or use chr()
print(f"Tab:{chr(9)}After")

3. Forgetting to Convert Types

# For complex objects, ensure they have a string representation
class MyClass:
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return f"MyClass(value={self.value})"

obj = MyClass(42)
print(f"Object: {obj}")
# Output: Object: MyClass(value=42)

4. Not Handling Missing Keys in Dictionaries

data = {"name": "Alice", "age": 30}

# Wrong: KeyError if key doesn't exist
# print(f"City: {data['city']}")  # KeyError

# Solution: Use .get() with default
print(f"City: {data.get('city', 'Unknown')}")
# Output: City: Unknown

5. Escaping Braces Incorrectly

# To include literal braces in an f-string, double them
print(f"Use {{curly braces}} in f-strings")
# Output: Use {curly braces} in f-strings

# This is an expression:
value = 42
print(f"{{This is an expression: {value}}}")
# Output: {This is an expression: 42}

Practice Exercises

Exercise 1: Create a Formatted Receipt

Create a function that takes a list of items with prices and quantities, and returns a formatted receipt string.

def create_receipt(items, tax_rate=0.08):
    """Create a formatted receipt from a list of items."""
    receipt_lines = []
    receipt_lines.append("=" * 40)
    receipt_lines.append(f"{'RECEIPT':^40}")
    receipt_lines.append("=" * 40)
    receipt_lines.append(f"{'Item':<20}{'Qty':>5}{'Price':>8}{'Total':>8}")
    receipt_lines.append("-" * 40)

    subtotal = 0
    for item_name, price, quantity in items:
        item_total = price * quantity
        subtotal += item_total
        receipt_lines.append(
            f"{item_name:<20}{quantity:>5}{price:>8.2f}{item_total:>8.2f}"
        )

    tax = subtotal * tax_rate
    total = subtotal + tax

    receipt_lines.append("-" * 40)
    receipt_lines.append(f"{'Subtotal:':<30}{subtotal:>10.2f}")
    receipt_lines.append(f"{'Tax (' + f'{tax_rate:.0%}' + '):':<30}{tax:>10.2f}")
    receipt_lines.append("=" * 40)
    receipt_lines.append(f"{'TOTAL:':<30}{total:>10.2f}")
    receipt_lines.append("=" * 40)

    return "\n".join(receipt_lines)


items = [
    ("Apple", 1.29, 3),
    ("Bread", 2.49, 1),
    ("Milk", 3.99, 2),
    ("Eggs", 4.99, 1),
]

print(create_receipt(items))
# Output:
# ========================================
#               RECEIPT
# ========================================
# Item                 Qty   Price   Total
# ----------------------------------------
# Apple                  3    1.29    3.87
# Bread                  1    2.49    2.49
# Milk                   2    3.99    7.98
# Eggs                   1    4.99    4.99
# ----------------------------------------
# Subtotal:                      19.33
# Tax (8%):                       1.55
# ========================================
# TOTAL:                         20.88
# ========================================

Exercise 2: Build a Log Formatter

Create a function that formats log messages with timestamps, levels, and context information.

from datetime import datetime

def format_log(level, message, context=None):
    """Format a log message with timestamp, level, and optional context."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    colors = {
        "DEBUG": "\033[90m",
        "INFO": "\033[94m",
        "WARNING": "\033[93m",
        "ERROR": "\033[91m",
        "CRITICAL": "\033[95m",
    }
    reset = "\033[0m"

    context_str = f" [{context}]" if context else ""
    color = colors.get(level, "")
    formatted = f"{color}{timestamp} [{level}]{reset}{context_str}: {message}"

    return formatted


print(format_log("INFO", "Server started on port 8080"))
print(format_log("WARNING", "High memory usage", "system"))
print(format_log("ERROR", "Failed to connect to database", "db"))
print(format_log("DEBUG", f"Processing {1000} records", "worker"))

Exercise 3: Create a Table Formatter

Create a function that formats data as a nicely aligned ASCII table with customizable alignment.

def create_table(headers, rows, alignments=None):
    """Create a formatted ASCII table."""
    if alignments is None:
        alignments = ['<'] * len(headers)

    # Calculate column widths
    col_widths = []
    for i, header in enumerate(headers):
        width = len(str(header))
        for row in rows:
            width = max(width, len(str(row[i])))
        col_widths.append(width + 2)

    def format_cell(value, width, align):
        return f"{str(value):{align}{width}}"

    separator = "+" + "+".join("-" * w for w in col_widths) + "+"
    header_row = "|" + "|".join(
        format_cell(h, col_widths[i], alignments[i])
        for i, h in enumerate(headers)
    ) + "|"

    data_rows = []
    for row in rows:
        formatted_row = "|" + "|".join(
            format_cell(row[i], col_widths[i], alignments[i])
            for i in range(len(headers))
        ) + "|"
        data_rows.append(formatted_row)

    table_lines = [separator, header_row, separator] + data_rows + [separator]
    return "\n".join(table_lines)


headers = ["Name", "Age", "Score", "Grade"]
rows = [
    ["Alice", 25, 95.5, "A"],
    ["Bob", 30, 87.3, "B+"],
    ["Charlie", 22, 92.1, "A-"],
    ["Diana", 28, 78.9, "B"],
]
alignments = ['<', '^', '>', '^']

print(create_table(headers, rows, alignments))
# Output:
# +---------+-----+--------+------+
# | Name    | Age |  Score | Grade|
# +---------+-----+--------+------+
# | Alice   |  25 |   95.5 |   A  |
# | Bob     |  30 |   87.3 |  B+  |
# | Charlie |  22 |   92.1 |  A-  |
# | Diana   |  28 |   78.9 |   B  |
# +---------+-----+--------+------+

Key Takeaways

  • f-strings are the preferred string formatting method in Python 3.6+ due to their readability, conciseness, and performance
  • Use the = specifier (Python 3.8+) for quick debugging to see both expression and value
  • Format specifiers follow the mini-language: [[fill]align][sign][z][#][0][width][grouping_option][.precision][type]
  • f-strings support any valid Python expression, including function calls, method calls, and comprehensions
  • Triple-quoted f-strings enable multiline formatted strings
  • Nested f-strings allow complex formatting patterns
  • Avoid backslashes inside f-string expressions (Python less than 3.12)
  • f-strings are typically 2-3x faster than .format() and faster than % formatting
  • Always handle missing keys in dictionaries using .get() with defaults
  • Use literal braces by doubling them: {{ and }}

Advertisement

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement