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.
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.
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.
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.
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"₹{amount:,.2f}") # ₹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:,}"
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.
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.
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'}"
)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.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
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.
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.
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}°F"
return f"{self.celsius:.1f}°C"
t = Temperature(25)
print(f"{t}") # 25.0°C
print(f"{t:f}") # 77.0°F — custom format specThe __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