String Formatting with f-strings in Python
F-strings (formatted string literals) let you insert variables and expressions directly into your strings. They arrived in Python 3.6 and quickly became the preferred way to format strings. If you’ve been using older methods, you’ll see why.
Basic f-string Syntax
An f-string starts with an f before the opening quote. Inside the string, you place variables inside curly braces {} and Python substitutes them with their values.
name = "Alice"
age = 30
message = f"Hello, my name is {name} and I am {age} years old."
print(message)
# Hello, my name is Alice and I am 30 years old.
The leading f character before the opening quote signals Python to scan the string for curly-brace expressions and substitute their evaluated values at runtime. Without that f prefix, the curly braces would appear literally in the output instead of being treated as expression placeholders.
You can nest quotes inside f-strings by alternating between single and double quotes, avoiding the need to escape characters manually:
greeting = f"She said: \"Hello {name}!\""
print(greeting)
# She said: "Hello Alice!"
Embedding Expressions
The curly braces accept far more than simple variable names. Any valid Python expression can appear inside the braces, and Python evaluates each one at runtime before inserting the result into the string. This includes arithmetic, method calls, function invocations, and even ternary expressions. This makes f-strings more than just a formatting tool. They become a compact way to inline computation directly into output strings.
price = 19.99
quantity = 3
# Direct calculation inside the f-string
total = f"Total: ${price * quantity:.2f}"
print(total)
# Total: $59.97
# String methods work too
word = "python"
print(f"Uppercase: {word.upper()}")
# Uppercase: PYTHON
# Function calls are fine
def discount(price):
return price * 0.9
original = 100
print(f"After discount: ${discount(original):.2f}")
# After discount: $90.00
Python evaluates whatever is inside the braces before inserting it into the string. This means you can do calculations, call functions, and use methods all in one place.
Format Specifiers for Numbers
Format specifiers go after a colon inside the curly braces. They control how numbers appear — decimal places, padding, alignment, and more.
Controlling Decimal Places
pi = 3.14159265
# Round to 2 decimal places
print(f"Pi is approximately {pi:.2f}")
# Pi is approximately 3.14
# Round to 4 decimal places
print(f"Pi is approximately {pi:.4f}")
# Pi is approximately 3.1416
The format specifier “.2f” means fixed-point notation with exactly two digits after the decimal point. It rounds the value instead of truncating, and it always shows the specified number of decimal places even when they would be trailing zeros. This specifier syntax is shared across all Python string formatting methods, so knowledge of format specifiers transfers between f-strings, the older percent operator, and the format method.
Padding Integers
for i in range(1, 6):
print(f"Item {i:02d}") # Pad with leading zeros to 2 characters
# Item 01
# Item 02
# Item 03
# Item 04
# Item 05
The format string “02d” specifies a decimal integer padded with leading zeros to a minimum width of two characters. Leading zeros work well for fixed-width identifiers like invoice numbers, order codes, or any sequence where uniform string length aids readability or sorting. For padding with spaces instead of zeros, you would omit the zero: just “2d” right-aligns the number within two character positions.
Padding for Alignment
products = [
("Apples", 0.50),
("Strawberries", 3.99),
("Dragon Fruit", 7.50)
]
print(f"{'Product':<15} {'Price':>10}")
print("-" * 25)
for name, price in products:
print(f"{name:<15} ${price:>9.2f}")
# Product Price
# -------------------------
# Apples $ 0.50
# Strawberries $ 3.99
# Dragon Fruit $ 7.50
The less-than sign aligns text to the left of the available width, and the greater-than sign aligns to the right. The numeric value before the alignment character sets the minimum column width in characters. This produces neatly formatted tables directly from f-strings without needing external formatting libraries for ad hoc console output.
Percentages
correct = 42
total = 50
percentage = correct / total
print(f"You got {percentage:.1%}")
# You got 84.0%
# Or show it as a fraction
print(f"{correct}/{total} = {percentage:.2%}")
# 42/50 = 84.00%
The percent format specifier multiplies the value by one hundred and appends a literal percent sign, all within the f-string expression. The number before the percent sign controls decimal precision. For example, “.1%” rounds to one decimal place, and “.2%” to two. This is useful for displaying ratios, test pass rates, or any proportion where a percentage is the natural unit.
F-strings vs Other Methods
Python has supported string formatting for decades, and f-strings are the latest in a line of approaches. Comparing them side by side shows why f-strings have become the default choice in modern Python codebases.
The Old % Formatting
name = "Bob"
# Old style
message = "Hello, %s" % name
# F-string (cleaner)
message = f"Hello, {name}"
The percent operator works for simple cases but becomes difficult to read when you have more than two or three values to substitute. Each placeholder requires a format specifier character, and the values must be provided as a tuple in the exact same order as the placeholders appear in the string. Getting the order wrong produces a confusing result instead of an error, which makes the percent operator error-prone for all but the simplest formatting tasks.
The str.format() Method
name = "Carol"
age = 25
# Using format()
message = "Hello, {}! You are {} years old.".format(name, age)
# Using f-string
message = f"Hello, {name}! You are {age} years old."
The format method improved on the percent operator by supporting named and positional placeholders, but it still separates the template from the values. The reader has to jump between the placeholder positions in the template and the argument list at the end to understand what each value represents. F-strings eliminate this indirection by putting the variables directly into the template at the point where they appear.
Why F-strings Won
F-strings are shorter, more readable, and more flexible. They let you see exactly what values go where. They support all the formatting options of .format() but with cleaner syntax. Most Python developers now use f-strings for everything.
There’s one case where you might still use .format(): when building strings dynamically from a dictionary.
data = {"name": "Dave", "score": 95}
# Using format with ** unpacking (cleaner here)
message = "Name: {name}, Score: {score}".format(**data)
print(message)
# Name: Dave, Score: 95
For everything else, f-strings are your best choice.
Common Gotchas
Watch out for these when starting with f-strings.
Curly braces are special. If you need literal curly braces in your output, double them:
print(f"Use {{ and }} for literal braces")
# Use { and } for literal braces
F-strings are evaluated at runtime with performance comparable to regular string concatenation. They are not slower than the alternatives, and in many cases they are faster than the format method because the compiler can optimize the f-string into efficient bytecode.
The expression inside an f-string cannot contain backslash escape sequences, which means you cannot use line continuation inside the braces:
# This won't work:
# text = f"{line.strip() for line in lines}"
# Do this instead:
text = "".join(line.strip() for line in lines)
Next Steps
Now you know how to format strings cleanly with f-strings. This skill comes up constantly in real code — logging, user messages, file names, debugging output.
Continue with the next tutorial in the series to learn about working with files and reading/writing data.