Skip to content

Python F-Strings: The Complete Guide to String Formatting

Updated on

String formatting is one of the most common operations in Python, yet it has been a source of frustration for years. The % operator uses cryptic format codes borrowed from C. The .format() method scatters values far from where they appear in the template. String concatenation with + forces you to manually convert types and produces messy, hard-to-read code. Every approach feels like it adds friction to what should be a simple task: putting a value inside a string.

The problem gets worse in real projects. You need to format currency with two decimal places, align columns in a report, pad numbers with leading zeros, display percentages, and format timestamps -- all while keeping the code readable. With older formatting methods, you end up maintaining two parallel structures: the template string and the argument list. A single reordering mistake breaks everything silently, producing wrong output instead of an error.

Python 3.6 introduced f-strings (formatted string literals), and they changed string formatting permanently. By prefixing a string with f and embedding expressions directly inside {curly braces}, f-strings keep values next to where they appear. They are faster than every other formatting method. They support the full range of format specifications. And in Python 3.8, they gained a built-in debugging feature with the = specifier. This guide covers every f-string capability with practical, runnable examples.

📚

What Are F-Strings?

F-strings, formally called "formatted string literals," were introduced in PEP 498 (opens in a new tab) and shipped with Python 3.6. The concept is simple: prefix any string with f (or F), and Python evaluates any expression inside curly braces {} at runtime, converting the result to a string and inserting it into the output.

name = "Alice"
age = 30
 
greeting = f"My name is {name} and I am {age} years old."
print(greeting)
# Output: My name is Alice and I am 30 years old.

The f prefix works with single quotes, double quotes, and triple quotes:

single = f'Hello, {name}'
double = f"Hello, {name}"
triple_double = f"""Hello, {name}"""
triple_single = f'''Hello, {name}'''

Under the hood, Python compiles f-strings into a series of string concatenation and format() operations at parse time. This is why f-strings are faster than calling .format() yourself -- the interpreter optimizes the operation directly.

Basic F-String Syntax

The core syntax is straightforward: write f"text {expression} more text". Python evaluates the expression, calls str() on the result, and inserts it into the string.

# Variables
city = "Tokyo"
population = 13960000
 
print(f"{city} has a population of {population}.")
# Output: Tokyo has a population of 13960000.
 
# You can use either f or F
print(F"Welcome to {city}!")
# Output: Welcome to Tokyo!

You can place any number of expressions inside a single f-string:

first = "Grace"
last = "Hopper"
title = "Rear Admiral"
year = 1906
 
bio = f"{title} {first} {last}, born {year}, lived to {1992 - year} years."
print(bio)
# Output: Rear Admiral Grace Hopper, born 1906, lived to 86 years.

Expressions Inside F-Strings

F-strings do not just insert variables. They evaluate any valid Python expression between the braces. This includes arithmetic, method calls, function calls, ternary expressions, list indexing, dictionary access, and more.

Arithmetic and Math

x = 15
y = 4
 
print(f"{x} + {y} = {x + y}")
print(f"{x} / {y} = {x / y:.2f}")
print(f"{x} ** {y} = {x ** y}")
print(f"{x} % {y} = {x % y}")
# Output:
# 15 + 4 = 19
# 15 / 4 = 3.75
# 15 ** 4 = 50625
# 15 % 4 = 3

Method Calls

text = "  hello, python world  "
 
print(f"Stripped: '{text.strip()}'")
print(f"Title case: '{text.strip().title()}'")
print(f"Upper: '{text.strip().upper()}'")
print(f"Replace: '{text.strip().replace('python', 'f-string')}'")
# Output:
# Stripped: 'hello, python world'
# Title case: 'Hello, Python World'
# Upper: 'HELLO, PYTHON WORLD'
# Replace: 'hello, f-string world'

Function Calls

def calculate_bmi(weight_kg, height_m):
    return weight_kg / (height_m ** 2)
 
weight = 70
height = 1.75
 
print(f"BMI: {calculate_bmi(weight, height):.1f}")
# Output: BMI: 22.9
 
# Built-in functions
items = [3, 1, 4, 1, 5, 9, 2, 6]
print(f"Items: {items}")
print(f"Count: {len(items)}, Sum: {sum(items)}, Min: {min(items)}, Max: {max(items)}")
# Output:
# Items: [3, 1, 4, 1, 5, 9, 2, 6]
# Count: 8, Sum: 31, Min: 1, Max: 9

Ternary (Conditional) Expressions

score = 85
status = f"Result: {'PASS' if score >= 60 else 'FAIL'}"
print(status)
# Output: Result: PASS
 
temperature = -5
print(f"Water is {'frozen' if temperature <= 0 else 'liquid' if temperature < 100 else 'steam'}.")
# Output: Water is frozen.

Dictionary and List Access

user = {"name": "Bob", "role": "engineer", "level": 3}
scores = [88, 92, 76, 95, 81]
 
print(f"{user['name']} is a level {user['level']} {user['role']}.")
# Output: Bob is a level 3 engineer.
 
print(f"First score: {scores[0]}, Last score: {scores[-1]}")
# Output: First score: 88, Last score: 81

Format Specifications: Numbers, Alignment, and Padding

The real power of f-strings shows in format specifications. After the expression, add a colon : followed by a format spec. The general syntax is:

{expression:[[fill]align][sign][#][0][width][grouping_option][.precision][type]}

This looks dense, but each piece is optional. Let's break it down with examples.

Decimal Places and Floating-Point Precision

The .Nf specifier controls how many decimal places a float displays:

pi = 3.141592653589793
 
print(f"Default:    {pi}")
print(f"2 decimals: {pi:.2f}")
print(f"4 decimals: {pi:.4f}")
print(f"0 decimals: {pi:.0f}")
print(f"8 decimals: {pi:.8f}")
# Output:
# Default:    3.141592653589793
# 2 decimals: 3.14
# 4 decimals: 3.1416
# 0 decimals: 3
# 8 decimals: 3.14159265

Thousands Separators

Large numbers become readable instantly with comma or underscore grouping:

revenue = 1234567890
small_amount = 1234.5
 
print(f"Revenue: {revenue:,}")
print(f"Revenue: {revenue:_}")
print(f"Amount: ${small_amount:,.2f}")
# Output:
# Revenue: 1,234,567,890
# Revenue: 1_234_567_890
# Amount: $1,234.50

Percentage Formatting

The % type multiplies the value by 100 and appends a percent sign:

completion = 0.876
error_rate = 0.034
growth = 1.25
 
print(f"Completion: {completion:.1%}")
print(f"Error rate: {error_rate:.2%}")
print(f"Growth: {growth:.0%}")
# Output:
# Completion: 87.6%
# Error rate: 3.40%
# Growth: 125%

Scientific Notation

avogadro = 6.022e23
planck = 6.626e-34
 
print(f"Avogadro: {avogadro:.3e}")
print(f"Planck: {planck:.4E}")
# Output:
# Avogadro: 6.022e+23
# Planck: 6.6260E-34

Integer Bases: Binary, Octal, Hexadecimal

value = 255
 
print(f"Decimal:     {value:d}")
print(f"Binary:      {value:b}")
print(f"Octal:       {value:o}")
print(f"Hex (lower): {value:x}")
print(f"Hex (upper): {value:X}")
print(f"With prefix: {value:#b}  {value:#o}  {value:#x}")
# Output:
# Decimal:     255
# Binary:      11111111
# Octal:       377
# Hex (lower): ff
# Hex (upper): FF
# With prefix: 0b11111111  0o377  0xff

Alignment and Padding

Alignment specifiers control how values sit within a given width. Use < for left, > for right, and ^ for center:

label = "Python"
 
print(f"|{label:<20}|")   # Left-align in 20 chars
print(f"|{label:>20}|")   # Right-align
print(f"|{label:^20}|")   # Center
print(f"|{label:*^20}|")  # Center with * fill
print(f"|{label:->20}|")  # Right with - fill
# Output:
# |Python              |
# |              Python|
# |       Python       |
# |*******Python*******|
# |--------------Python|

Zero-padding for numbers is common in report formatting:

for i in [1, 42, 100, 7, 999]:
    print(f"ID: {i:05d}")
# Output:
# ID: 00001
# ID: 00042
# ID: 00100
# ID: 00007
# ID: 00999

Building Aligned Tables

Combining alignment with format specs produces clean tables:

products = [
    ("Laptop Pro", 1299.99, 45),
    ("Wireless Mouse", 29.50, 230),
    ("USB-C Hub", 54.95, 120),
    ("Monitor 27in", 449.00, 67),
]
 
print(f"{'Product':<18} {'Price':>10} {'Stock':>7}")
print("-" * 37)
for name, price, stock in products:
    print(f"{name:<18} ${price:>9.2f} {stock:>7,}")
print("-" * 37)
 
total_value = sum(p * s for _, p, s in products)
print(f"{'Total Value':<18} ${total_value:>9,.2f}")
 
# Output:
# Product               Price   Stock
# -------------------------------------
# Laptop Pro         $ 1,299.99      45
# Wireless Mouse     $    29.50     230
# USB-C Hub          $    54.95     120
# Monitor 27in       $   449.00      67
# -------------------------------------
# Total Value        $91,498.05

Date and Time Formatting with F-Strings

F-strings accept strftime format codes directly, which means you can format datetime objects without calling .strftime():

from datetime import datetime, date, timedelta
 
now = datetime.now()
 
print(f"ISO format:  {now:%Y-%m-%d %H:%M:%S}")
print(f"US format:   {now:%m/%d/%Y}")
print(f"Long format: {now:%B %d, %Y at %I:%M %p}")
print(f"Day of week: {now:%A}")
print(f"Short month: {now:%b %d}")
# Output (varies by current time):
# ISO format:  2026-02-13 14:30:00
# US format:   02/13/2026
# Long format: February 13, 2026 at 02:30 PM
# Day of week: Friday
# Short month: Feb 13

This works with date objects too:

from datetime import date
 
birthday = date(1995, 8, 15)
today = date.today()
age_days = (today - birthday).days
 
print(f"Birthday: {birthday:%B %d, %Y}")
print(f"Days alive: {age_days:,}")
print(f"Years (approx): {age_days / 365.25:.1f}")
# Output:
# Birthday: August 15, 1995
# Days alive: 11,139
# Years (approx): 30.5

Multiline F-Strings

Triple-quoted f-strings let you build formatted blocks of text. Every line in a triple-quoted f-string can contain {} expressions:

project = "Data Pipeline"
owner = "Engineering Team"
status = "In Progress"
completion = 0.73
budget = 250000
spent = 182500
 
report = f"""
========================================
  Project Status Report
========================================
  Project:    {project}
  Owner:      {owner}
  Status:     {status}
  Completion: {completion:.0%}
  Budget:     ${budget:>12,}
  Spent:      ${spent:>12,}
  Remaining:  ${budget - spent:>12,}
========================================
"""
 
print(report)

You can also build multiline f-strings by concatenating multiple f-strings with implicit string concatenation (placing them next to each other with no operator):

name = "Alice"
role = "Data Scientist"
years = 5
 
bio = (
    f"Name: {name}\n"
    f"Role: {role}\n"
    f"Experience: {years} years\n"
    f"Seniority: {'Senior' if years >= 5 else 'Mid-level'}"
)
 
print(bio)
# Output:
# Name: Alice
# Role: Data Scientist
# Experience: 5 years
# Seniority: Senior

This second approach avoids the leading newline and indentation issues that sometimes come with triple-quoted strings.

The = Specifier for Debugging (Python 3.8+)

Python 3.8 added the = specifier to f-strings, and it is one of the most useful debugging features in the language. When you write f"{expression=}", Python prints both the expression text and its value.

x = 42
y = 17
 
print(f"{x=}")
print(f"{y=}")
print(f"{x + y=}")
print(f"{x * y=}")
# Output:
# x=42
# y=17
# x + y=59
# x * y=714

You can combine = with format specifiers:

price = 49.99
tax_rate = 0.0825
total = price * (1 + tax_rate)
 
print(f"{price=:.2f}")
print(f"{tax_rate=:.1%}")
print(f"{total=:.2f}")
# Output:
# price=49.99
# tax_rate=8.2%
# total=54.12

The = specifier preserves whitespace around it. Adding spaces before = formats the output:

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

Notice that for strings, = uses repr() by default (showing the quotes). For numbers, it uses str().

Debugging Complex Expressions

The = specifier works with any expression, making it a fast way to inspect intermediate values:

data = [23, 45, 12, 67, 34, 89, 56]
 
print(f"{len(data)=}")
print(f"{sum(data)=}")
print(f"{sum(data)/len(data)=:.2f}")
print(f"{sorted(data)=}")
print(f"{max(data) - min(data)=}")
# Output:
# len(data)=7
# sum(data)=326
# sum(data)/len(data)=46.57
# sorted(data)=[12, 23, 34, 45, 56, 67, 89]
# max(data) - min(data)=77

If you work in Jupyter notebooks and find yourself constantly adding print(f"{variable=}") calls while debugging, consider RunCell (opens in a new tab), an AI agent that runs directly inside Jupyter. RunCell understands the variables in your notebook's memory and can inspect, explain, and suggest fixes without you having to write debug statements manually. It is especially useful when stepping through data transformations where you need to verify intermediate values across multiple cells.

Nested F-Strings

You can nest f-strings inside f-strings. The most common use case is dynamic format specifications, where the width or precision comes from a variable:

# Dynamic precision
value = 3.14159265
for precision in range(1, 6):
    print(f"  {precision} decimals: {value:.{precision}f}")
# Output:
#   1 decimals: 3.1
#   2 decimals: 3.14
#   3 decimals: 3.142
#   4 decimals: 3.1416
#   5 decimals: 3.14159
# Dynamic width
header = "Report"
for width in [20, 30, 40]:
    print(f"|{header:^{width}}|")
# Output:
# |       Report       |
# |            Report            |
# |                 Report                 |
# Nested f-string for formatting a list
values = [1.234, 56.789, 0.001, 999.999]
decimals = 2
formatted = f"Results: {', '.join(f'{v:.{decimals}f}' for v in values)}"
print(formatted)
# Output: Results: 1.23, 56.79, 0.00, 1000.00

Use nesting sparingly. If an f-string requires more than one level of nesting, extract the inner logic into a variable or function for readability.

F-Strings vs Other String Formatting Methods

Python has four main approaches to string formatting. Here is a direct comparison:

Featuref-string f"...{x}...".format() "...{}...".format(x)% operator "...%s..." % xTemplate Template("...$x...")
Python Version3.6+2.6+AllAll
ReadabilityExcellent -- values inlineGood -- numbered/named placeholdersFair -- positional codesGood -- named placeholders
PerformanceFastest~1.5-2x slower~1.3-1.8x slowerSlowest
Arbitrary ExpressionsYesLimitedNoNo
Debug Specifier (=)Yes (3.8+)NoNoNo
Reusable TemplatesNoYesYesYes
Safe for User InputNoNoNoYes (safe_substitute)
Type FormattingFullFullPartialNone
name = "Charlie"
balance = 1234.56
 
# f-string (recommended)
print(f"{name} has ${balance:,.2f}")
 
# .format()
print("{} has ${:,.2f}".format(name, balance))
 
# % formatting
print("%s has $%,.2f" % (name, balance))  # Note: %  does not support comma grouping
 
# Template (from string module)
from string import Template
t = Template("$name has $balance")
print(t.substitute(name=name, balance=f"${balance:,.2f}"))

When to use each:

  • F-strings: Default choice for new code. Use whenever you are running Python 3.6+.
  • .format(): Use when you need reusable templates stored in variables or config files.
  • % formatting: Legacy code only. No reason to use in new projects.
  • Template: Use when the format string comes from untrusted user input (it cannot execute arbitrary expressions).

Performance Comparison

F-strings are faster because Python compiles them into optimized bytecode at parse time, avoiding the overhead of method calls and argument parsing that .format() and % require.

import timeit
 
name = "Alice"
age = 30
score = 95.5
 
# Benchmark each method with 1 million iterations
fstring_time = timeit.timeit(
    'f"{name} scored {score:.1f} at age {age}"',
    globals={"name": name, "age": age, "score": score},
    number=1_000_000
)
 
format_time = timeit.timeit(
    '"{} scored {:.1f} at age {}".format(name, score, age)',
    globals={"name": name, "age": age, "score": score},
    number=1_000_000
)
 
percent_time = timeit.timeit(
    '"%s scored %.1f at age %d" % (name, score, age)',
    globals={"name": name, "age": age, "score": score},
    number=1_000_000
)
 
concat_time = timeit.timeit(
    'name + " scored " + str(score) + " at age " + str(age)',
    globals={"name": name, "age": age, "score": score},
    number=1_000_000
)
 
print(f"f-string:      {fstring_time:.4f}s (baseline)")
print(f"str.format():  {format_time:.4f}s ({format_time/fstring_time:.2f}x slower)")
print(f"% formatting:  {percent_time:.4f}s ({percent_time/fstring_time:.2f}x slower)")
print(f"concatenation: {concat_time:.4f}s ({concat_time/fstring_time:.2f}x slower)")
 
# Typical results:
# f-string:      0.0850s (baseline)
# str.format():  0.1450s (1.71x slower)
# % formatting:  0.1100s (1.29x slower)
# concatenation: 0.1200s (1.41x slower)

The performance gap widens as expressions become more complex. In tight loops processing thousands of records, f-strings make a measurable difference.

Common Mistakes and Gotchas

Escaping Curly Braces

To include a literal { or } in an f-string, double it:

value = 42
 
# Literal braces around a value
print(f"{{{value}}}")
# Output: {42}
 
# JSON-like output
key = "name"
val = "Alice"
print(f'{{"{key}": "{val}"}}')
# Output: {"name": "Alice"}
 
# Just literal braces, no interpolation
print(f"Use {{variable}} syntax in f-strings")
# Output: Use {variable} syntax in f-strings

Backslashes Inside Expressions

You cannot use backslash characters inside the {} expression part of an f-string. This is a deliberate design choice for readability:

items = ["apple", "banana", "cherry"]
 
# This causes a SyntaxError:
# print(f"{'\\n'.join(items)}")
 
# Solution 1: assign to a variable first
newline = "\n"
print(f"{newline.join(items)}")
 
# Solution 2: use a separate operation
joined = "\n".join(items)
print(f"Items:\n{joined}")
 
# Solution 3: use chr()
print(f"{chr(10).join(items)}")

Quote Conflicts

When your f-string uses double quotes, expressions inside must use single quotes (or vice versa):

data = {"name": "Alice", "age": 30}
 
# Use single quotes inside when f-string uses double quotes
print(f"Name: {data['name']}, Age: {data['age']}")
 
# Or use double quotes inside when f-string uses single quotes
print(f'Name: {data["name"]}, Age: {data["age"]}')
 
# With triple quotes, you have more flexibility
print(f"""Name: {data["name"]}, Age: {data["age"]}""")

F-Strings Are Not Constants

F-strings are evaluated at runtime, which means you cannot use them as default argument values or module-level constants that depend on runtime state:

# This works but evaluates at call time, not definition time
def greet(name, template=None):
    if template is None:
        template = f"Hello, {name}!"  # Evaluated here
    return template
 
# For true templates, use str.format()
TEMPLATE = "Hello, {name}! You have {count} messages."
print(TEMPLATE.format(name="Alice", count=5))

Watch Out for Large Expressions

Just because you can put any expression in an f-string does not mean you should. Keep f-string expressions simple:

# Hard to read -- too much logic inside the f-string
result = f"{'%.2f' % (sum(x**2 for x in range(100)) / 100)}"
 
# Much better -- compute first, then format
mean_square = sum(x**2 for x in range(100)) / 100
result = f"{mean_square:.2f}"

Raw F-Strings

Combining r (raw) and f prefixes creates a raw f-string where backslashes are treated literally (no escape sequences) while expressions still get evaluated:

user = "admin"
folder = "projects"
 
# Raw f-string for Windows paths
path = rf"C:\Users\{user}\{folder}\data.csv"
print(path)
# Output: C:\Users\admin\projects\data.csv
 
# Useful for regex patterns with dynamic parts
import re
prefix = "user"
pattern = rf"\b{prefix}_\d+\b"
text = "Found user_123 and user_456 in the log"
matches = re.findall(pattern, text)
print(f"Matches: {matches}")
# Output: Matches: ['user_123', 'user_456']

Both rf"..." and fr"..." are valid -- the order of the prefixes does not matter.

F-Strings with Data Science Workflows

F-strings are invaluable in data science for formatting output, building dynamic labels, and creating readable summaries from numeric results.

Formatting Numeric Output

import statistics
 
data = [23.4, 45.1, 31.8, 52.3, 41.7, 38.9, 29.6, 47.2]
 
mean = statistics.mean(data)
median = statistics.median(data)
stdev = statistics.stdev(data)
cv = stdev / mean  # coefficient of variation
 
print(f"Sample size:    {len(data)}")
print(f"Mean:           {mean:.2f}")
print(f"Median:         {median:.2f}")
print(f"Std deviation:  {stdev:.2f}")
print(f"CV:             {cv:.1%}")
print(f"Range:          {min(data):.1f} - {max(data):.1f}")
# Output:
# Sample size:    8
# Mean:           38.75
# Median:         40.30
# Std deviation:  9.87
# CV:             25.5%
# Range:          23.4 - 52.3

Working with Pandas DataFrames

import pandas as pd
 
df = pd.DataFrame({
    "product": ["Widget A", "Widget B", "Widget C", "Widget D"],
    "revenue": [125000, 89000, 234000, 67000],
    "units": [1250, 2100, 980, 3400],
})
 
# Summary statistics with f-strings
total_rev = df["revenue"].sum()
avg_price = (df["revenue"] / df["units"]).mean()
 
print(f"Total Revenue:  ${total_rev:>12,}")
print(f"Avg Unit Price: ${avg_price:>12,.2f}")
print(f"Products:       {len(df):>12,}")
 
# Dynamic column formatting
for _, row in df.iterrows():
    unit_price = row["revenue"] / row["units"]
    print(f"  {row['product']:<12} | Revenue: ${row['revenue']:>9,} | Units: {row['units']:>5,} | $/unit: ${unit_price:.2f}")
 
# Output:
# Total Revenue:  $     515,000
# Avg Unit Price: $       56.00
# Products:              4
#   Widget A     | Revenue: $  125,000 | Units: 1,250 | $/unit: $100.00
#   Widget B     | Revenue: $   89,000 | Units: 2,100 | $/unit: $42.38
#   Widget C     | Revenue: $  234,000 | Units:   980 | $/unit: $238.78
#   Widget D     | Revenue: $   67,000 | Units: 3,400 | $/unit: $19.71

When your data analysis goes beyond text summaries and you need interactive exploration, PyGWalker (opens in a new tab) can transform any pandas DataFrame into a Tableau-like interactive visualization with a single line of code. Instead of manually formatting numbers for static reports, you can drag and drop columns to build charts, apply filters, and explore patterns visually -- all within your Jupyter notebook.

import pandas as pd
import pygwalker as pyg
 
# After preparing your DataFrame with well-formatted columns
df = pd.DataFrame({
    "Month": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
    "Revenue": [125000, 134000, 158000, 142000, 167000, 189000],
    "Growth": [0.05, 0.072, 0.179, -0.101, 0.176, 0.132],
})
 
# Add formatted columns for display
df["Revenue_Display"] = df["Revenue"].apply(lambda x: f"${x:,.0f}")
df["Growth_Display"] = df["Growth"].apply(lambda x: f"{x:+.1%}")
 
# Launch interactive visualization
# pyg.walk(df)

Formatting Model Results

# Formatting machine learning results
metrics = {
    "accuracy": 0.9234,
    "precision": 0.8891,
    "recall": 0.9456,
    "f1_score": 0.9164,
    "auc_roc": 0.9678,
}
 
print("Model Evaluation Results")
print("=" * 35)
for metric, value in metrics.items():
    bar_length = int(value * 20)
    bar = "#" * bar_length + "-" * (20 - bar_length)
    print(f"  {metric:<12} {value:.4f}  [{bar}]")
# Output:
# Model Evaluation Results
# ===================================
#   accuracy     0.9234  [##################--]
#   precision    0.8891  [#################---]
#   recall       0.9456  [##################--]
#   f1_score     0.9164  [##################--]
#   auc_roc      0.9678  [###################-]

Conversion Flags: !s, !r, and !a

F-strings support three conversion flags that control how the expression value is converted before formatting:

  • !s calls str() on the value (default behavior)
  • !r calls repr() on the value
  • !a calls ascii() on the value
text = "Hello\tWorld"
name = "Caf\u00e9"
 
print(f"str:   {text!s}")
print(f"repr:  {text!r}")
print(f"ascii: {name!a}")
# Output:
# str:   Hello	World
# repr:  'Hello\tWorld'
# ascii: 'Caf\xe9'

The !r flag is particularly useful for debugging, as it shows the raw representation including quotes and escape characters. It works nicely alongside the = specifier.

Frequently Asked Questions

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

F-strings (formatted string literals) are a string formatting mechanism introduced in Python 3.6 through PEP 498. They let you embed Python expressions directly inside string literals by prefixing the string with f and placing expressions inside curly braces. F-strings are evaluated at runtime, offer the best readability among all Python string formatting options, and are faster than str.format() and % formatting because they compile into optimized bytecode at parse time.

How do I format numbers with commas and decimal places in f-strings?

Use the format specification after a colon inside the braces. For commas as thousands separators, use :, -- for example, f"{1234567:,}" produces "1,234,567". For decimal places, use :.Nf where N is the number of digits -- for example, f"{3.14159:.2f}" gives "3.14". Combine both: f"{1234.5:,.2f}" outputs "1,234.50". For percentages, use :.N% which multiplies by 100 and appends a percent sign: f"{0.85:.1%}" produces "85.0%".

What is the f-string = specifier and how does it help with debugging?

The = specifier was added in Python 3.8 and prints both the expression text and its value. Writing f"{variable=}" outputs variable=value. This eliminates the need to type the variable name twice when debugging. It works with any expression: f"{len(data)=}" outputs len(data)=42. You can combine it with format specs: f"{price=:.2f}" outputs price=49.99. It is one of the fastest ways to inspect intermediate values during development.

Can I use f-strings with multiline strings?

Yes. Use triple quotes (f"""...""" or f'''...''') for multiline f-strings. Every line can contain {expression} placeholders. Alternatively, you can concatenate multiple f-strings using implicit string concatenation by placing them adjacent to each other in parentheses: (f"line 1: {x}\n" f"line 2: {y}\n"). The triple-quote approach is best for templates and reports; the concatenation approach avoids leading newline issues.

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

Yes. F-strings are consistently the fastest string formatting method in Python. In benchmarks, f-strings typically run 1.5 to 2 times faster than str.format() and 1.3 to 1.8 times faster than % formatting. The speed advantage comes from compile-time optimization: Python converts f-strings directly into efficient bytecode rather than performing runtime method lookups and argument parsing. The difference is measurable in tight loops and large-scale data processing.

How do I include literal curly braces in an f-string?

Double the braces. Use {{ for a literal { and }} for a literal }. For example, f"{{value}}" outputs {value} as plain text. To wrap an interpolated value in braces, use f"{{{variable}}}" -- the outer doubled braces produce literal braces, while the inner single braces trigger interpolation. This is commonly needed when generating JSON strings, CSS, or any output that uses curly braces in its syntax.

Conclusion

F-strings are the standard way to format strings in modern Python. They combine the best readability of any formatting approach with the best performance, and they support the full range of format specifications: decimal precision, thousands separators, percentages, scientific notation, alignment, padding, date formatting, and base conversions.

The key things to remember:

  • Prefix with f and put expressions inside {braces}.
  • Use : followed by a format spec for precise control over output (:.2f, :,, :.1%, :<20).
  • Use = after the expression for debugging (f"{variable=}"). Requires Python 3.8+.
  • Escape literal braces by doubling them: {{ and }}.
  • Do not put backslashes inside the {} expression. Assign to a variable first.
  • Keep expressions simple. If the logic inside {} is hard to read, extract it.
  • Use .format() only when you need reusable templates stored as variables.

F-strings handle everything from quick print() debugging to production report generation. Once you adopt them, there is no reason to go back to older formatting methods. Whether you are building data pipelines, formatting ML model outputs, or generating financial reports, f-strings deliver clean, fast, readable code.

📚