String Formatting with f-strings in Python

· 5 min read · Updated March 7, 2026 · beginner
strings formatting f-strings beginner

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 f tells Python to treat this as a formatted string. Without it, you’d get literal curly braces in your output.

You can also use f-strings with double quotes inside single quotes, or vice versa:

greeting = f"She said: \"Hello {name}!\""
print(greeting)
# She said: "Hello Alice!"

Embedding Expressions

The curly braces don’t just hold variable names. They can contain any valid Python expression. This is where f-strings become powerful.

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 .2f means “fixed-point notation with 2 decimal places.” This works exactly like the old % formatting but sits inside the f-string cleanly.

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 02d means “integer, padded to 2 digits with zeros.” This is perfect for generating IDs, invoice numbers, or anything that needs consistent width.

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 < aligns left, > aligns right. The number before the alignment specifies minimum width. This creates neat tables in your 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 % specifier multiplies by 100 and adds the percent sign automatically.

F-strings vs Other Methods

Python has had string formatting for decades. Here’s how f-strings compare.

The Old % Formatting

name = "Bob"
# Old style
message = "Hello, %s" % name
# F-string (cleaner)
message = f"Hello, {name}"

The % operator works but gets messy with multiple values. You have to use %s, %d, %f as placeholders and pass values in a tuple. F-strings are readable by comparison.

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 introduced named placeholders, which was an improvement. But f-strings are shorter and let you reference variables directly without passing them as arguments.

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, so they have the same performance as regular string concatenation. They’re not slower.

The expression inside f-strings can’t contain backslashes (line continuation characters). Use a variable if you need complex logic:

# 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.