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(), andstring.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}}