🐍 Python Course · Stage 1 · Lesson 4 of 89 · f-strings
Course Overview →

🟩 Start with the Beginner tab. Read it fully before moving on.

← Variables & Types String Methods →
thecodex.expert · The Codex Family of Knowledge
Python

f-strings in Python

f-strings (formatted string literals) embed expressions directly inside string literals using {} — the fastest, most readable way to format strings in modern Python.

Python 3.13 PEP 498 Last verified:
Canonical Definition

An f-string (formatted string literal, PEP 498) is a string prefixed with f or F whose contents may contain replacement fields — expressions enclosed in curly braces {} that are evaluated at runtime and formatted into the string. f-strings are evaluated at runtime and are faster than str.format() or %-formatting.

🟩 Beginner

The modern way to format strings

What you'll learn: How to embed variables and expressions directly inside strings with f"{}", and how to control formatting — decimal places, padding, thousands separators, and percentages.

How to read this tab: Open the Python Playground and try each format specifier as you read. Type f"{3.14159:.2f}" and see 3.14. Formatting is best learned by experimenting with the specifiers.

⏱ 25 min📄 2 sections🔶 Prerequisite: Variables & Types
The idea

An f-string lets you drop variables and expressions directly into a string by putting them in {curly braces}. Put an f before the opening quote, and everything in braces is evaluated and inserted.

f-strings are the modern standard — use them, not the older methods. Python has three string formatting methods: %-formatting (oldest, from C), str.format() (Python 2.6+), and f-strings (Python 3.6+). f-strings are the most readable and the fastest. Use them for all new code. You only need to recognise the older two when reading existing code.

What is an f-string?

Introduced in Python 3.6 (PEP 498), f-strings are the modern standard for string formatting. They are more readable and faster than the older % formatting and str.format() methods.

Pythonfstring_basics.py
name = "Priya"
age = 30

# f-string — put f before the quote, expressions in {}
greeting = f"Hello, {name}! You are {age} years old."
print(greeting)   # Hello, Priya! You are 30 years old.

# Any expression works inside the braces
print(f"Next year you will be {age + 1}.")   # Next year you will be 31.
print(f"Your name has {len(name)} letters.")  # Your name has 5 letters.

# Compare with older methods (avoid these in new code)
old1 = "Hello, %s! You are %d." % (name, age)      # %-formatting (oldest)
old2 = "Hello, {}! You are {}.".format(name, age)  # str.format()
new  = f"Hello, {name}! You are {age}."            # f-string (best)
💡

The format spec comes after a colon: f"{value:spec}". The most useful specs: .2f (2 decimal places), , (thousands separator), .1% (percentage), <10/>10/^10 (left/right/center align in width 10). Combine them: {value:>10,.2f} right-aligns a number with thousands separators and 2 decimals in a 10-char field.

Format specifiers

After the expression, add a colon and a format specifier to control how the value is displayed — decimal places, padding, alignment, number bases, and more.

Pythonfstring_format.py
pi = 3.14159265

# Decimal places — :.2f means 2 decimal places, float
print(f"{pi:.2f}")        # 3.14
print(f"{pi:.4f}")        # 3.1416

# Thousands separator
print(f"{1234567:,}")     # 1,234,567

# Percentage
print(f"{0.875:.1%}")     # 87.5%

# Padding and alignment (width 10)
print(f"{'left':<10}|")   # left      |
print(f"{'right':>10}|")  #      right|
print(f"{'center':^10}|") #   center  |

# Number bases
print(f"{255:b}")         # 11111111  (binary)
print(f"{255:x}")         # ff        (hex)
print(f"{255:o}")         # 377       (octal)

# Combine: pad a float to width 8, 2 decimals
print(f"{pi:8.2f}")       #     3.14

# Currency formatting (Indian Rupees example)
amount = 1234567.5
print(f"&#8377;{amount:,.2f}")   # &#8377;1,234,567.50

✅ Beginner tab complete

  • I can embed a variable in a string with f"{variable}"
  • I can embed an expression with f"{a + b}"
  • I can format to 2 decimal places with f"{value:.2f}"
  • I can add thousands separators with f"{number:,}"

Continue to functools →

🔵 Intermediate

Debugging, conditionals, and method calls

What you'll learn: The = debugging specifier (Python 3.8+), conditional expressions inside f-strings, calling methods inline, nested format specifiers, and handling quotes and literal braces.

How to read this tab: The f"{x=}" debugging form is the most immediately useful feature here — start using it in place of print("x =", x) right away.

⏱ 20 min📄 1 section🔶 Prerequisite: Beginner tab
💡

f"{x=}" is the debugging superpower. Added in Python 3.8, the = specifier prints both the expression text and its value: f"{user_count=}" produces user_count=42. This replaces the tedious print("user_count =", user_count) pattern. Use it constantly while debugging.

Advanced f-string features

f-strings support several powerful features: the = debugging specifier (Python 3.8+), nested expressions, conditional logic, and calling methods inline.

Pythonfstring_advanced.py
x = 42
y = 8

# The = specifier (Python 3.8+) — prints "expr=value", great for debugging
print(f"{x=}")           # x=42
print(f"{x + y=}")       # x + y=50
print(f"{x * y = }")     # x * y = 336  (spaces preserved)

# Conditional expressions inside f-strings
score = 75
print(f"Result: {'PASS' if score >= 50 else 'FAIL'}")  # Result: PASS

# Calling methods
name = "priya kumar"
print(f"{name.title()}")        # Priya Kumar
print(f"{name.upper()}")        # PRIYA KUMAR

# Nested f-strings and dynamic format specs
width = 10
value = 3.14159
print(f"{value:{width}.2f}")    # width comes from a variable

# Accessing dict values and object attributes
user = {"name": "Arjun", "age": 28}
print(f"{user['name']} is {user['age']}")   # Arjun is 28

# Multi-line f-strings
report = (
    f"Name: {name.title()}\n"
    f"Score: {score}\n"
    f"Status: {'PASS' if score >= 50 else 'FAIL'}"
)
Commonly confused
Quotes inside f-strings. Before Python 3.12, you couldn't reuse the same quote type inside an f-string's braces. f"{d["key"]}" was a syntax error — you had to use different quotes: f"{d['key']}". Python 3.12 (PEP 701) lifted this restriction, but for compatibility, still alternate quote types.
Literal braces. To include an actual brace character, double it: f"{{literal braces}}" produces {literal braces}. A single brace is interpreted as a replacement field and will error if it doesn't contain a valid expression.

✅ Intermediate tab complete

  • I can use f"{x=}" to print both the expression and its value for debugging
  • I can put a conditional expression inside an f-string
  • I can call methods inside braces: f"{name.upper()}"
  • I know to double braces {{ }} for literal brace characters

Continue to functools →

🔴 Expert

Compilation, bytecode, and __format__

What you'll learn: How CPython compiles f-strings to FORMAT_VALUE and BUILD_STRING bytecode (no runtime parsing). PEP 701's grammar formalisation in Python 3.12. The __format__ protocol that makes format specifiers extensible.

How to read this tab: Use dis.dis() on a function with an f-string and compare with one using .format(). The bytecode difference shows why f-strings are faster.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 1
📎

f-strings are a compile-time feature, not a runtime function. When Python compiles your code, it translates the f-string into direct concatenation and format calls. There is no format string to parse at runtime and no dictionary of arguments to look up — which is exactly why f-strings outperform str.format() and %-formatting.

How f-strings are compiled

Unlike str.format(), which is a runtime method call, f-strings are parsed and compiled by CPython at compile time into a series of string concatenations and format() calls. This is why they are faster — there is no runtime parsing of a format string or dictionary lookup of arguments.

Pythonfstring_internals.py
import dis

def with_fstring(name, age):
    return f"{name} is {age}"

# The bytecode uses FORMAT_VALUE and BUILD_STRING —
# no method call, no format-string parsing at runtime
dis.dis(with_fstring)
# LOAD_FAST name
# FORMAT_VALUE 0
# LOAD_CONST ' is '
# LOAD_FAST age
# FORMAT_VALUE 0
# BUILD_STRING 3
# RETURN_VALUE

# PEP 701 (Python 3.12) reimplemented f-string parsing in the
# formal grammar (PEG parser), enabling:
# - Reusing quote characters: f"{d["key"]}"
# - Multi-line expressions and comments inside braces
# - Arbitrary nesting depth
# - Backslashes inside the expression part

# The __format__ protocol: format specifiers are passed to
# the value's __format__ method
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    def __format__(self, spec):
        if spec == "f":  # fahrenheit
            return f"{self.celsius * 9/5 + 32:.1f}&#176;F"
        return f"{self.celsius:.1f}&#176;C"

t = Temperature(25)
print(f"{t}")     # 25.0&#176;C
print(f"{t:f}")   # 77.0&#176;F  — custom format spec

The __format__ protocol is what makes format specifiers extensible: when you write f"{value:spec}", Python calls type(value).__format__(value, "spec"). Built-in types implement this for their format mini-languages (the float format spec, the datetime format spec, etc.), and your own classes can too.

✅ Expert tab complete

  • I know f-strings compile to FORMAT_VALUE/BUILD_STRING bytecode, not a runtime method call
  • I know f-strings are faster than .format() and %-formatting
  • I can implement __format__ on my own class to support custom format specifiers

Continue to functools →

Sources

1
PEP 498 — Literal String Interpolation (f-strings). peps.python.org/pep-0498/.
2
PEP 701 — Syntactic formalization of f-strings (Python 3.12). peps.python.org/pep-0701/.
3
Python Language Reference §2.4.3 — Formatted string literals. docs.python.org/3/reference/lexical_analysis.html#f-strings.
4
Python Library Reference — Format Specification Mini-Language. docs.python.org/3/library/string.html#formatspec.
Source confidence: High Last verified: Primary source: PEP 498