String Formatting
f-strings, str.format, number formatting, and alignment.
What you will be able to do
- Build strings from text and values with f-strings
- Read and write a format spec, rather than memorising examples
- Format money, percentages, padded IDs, and aligned columns
- Put expressions and method calls inside an f-string - and know when not to
- Use f"{value=}" and !r while debugging
- Explain why formatting changes the display and never the value
The idea, in plain English
Almost every message a program produces mixes fixed text with values that change: a name, a total, a percentage. String formatting is how you build those, and Python has accumulated four ways of doing it over the years.
Concatenation came first and is the worst of them - it needs str() around every number, and by the third variable the quotes and plus signs outnumber the words. % formatting came next, then str.format(), and since Python 3.6 the f-string, which is what you should write today.
An f-string is an ordinary string with an f before the opening quote. Inside it, anything in braces is evaluated and its result dropped into place. Add a colon and you can say how it should look as well as what it is.
That colon is the part worth learning properly. Everything after it is a small language of its own - the same one used by str.format() and by format() - and once you can read it you stop looking up "python two decimal places" forever.
Worked example: Turning 1250000.567 into the line "Total: 1,250,000.57".
Four ways, and the one to use
All four produce the same string. They differ in how much of what you wrote is about the values and how much is punctuation holding it together.
Write f-strings. Learn to read % formatting, because it survives in logging calls and older code, and recognise str.format() when you meet it - it is still the right tool when the template and the values are separated, such as a message string loaded from a config file.
Concatenation"Hello " + name + ", you are " + str(age) - every number needs str(), and it gets unreadable fast.% formatting"Hello %s, you are %d" % (name, age) - the oldest style. Still common in logging: log.info("user %s", uid).str.format()"Hello {}, you are {}".format(name, age) - useful when the template is defined away from the values.f-stringf"Hello {name}, you are {age}" - shortest, fastest, and the values sit where they appear. Use this.Tip: f-strings are evaluated where they are written, so they cannot be used as a reusable template. When you need a template filled in later, str.format() is the right tool.
The format spec, decoded
Everything after the colon follows one grammar, and it is shorter than it looks: [fill][align][sign][width][,][.precision][type]. Every part is optional, and you will rarely use more than three at once.
Read f"{total:>12,.2f}" left to right: right-align, in a field 12 wide, with thousands separators, to two decimal places, as a fixed-point number. That is one expression doing what four lines of string surgery used to.
One default worth knowing: in a width field, strings align left and numbers align right. That is usually what you want for a table, and it is confusing the first time you set a width and things move in different directions.
fillAny character to pad with. Needs an align after it: :*^20 centres and pads with asterisks.align< left, > right, ^ centre. Strings default to left, numbers to right.sign+ shows a plus on positives, which lines up columns of gains and losses.widthMinimum total characters. :05 pads an integer with zeros to width 5.,Thousands separators. Use _ instead for 1_250_000 if you prefer..precisionDigits after the point for f, significant digits for g, characters for strings.typef fixed-point, % percentage, e scientific, d integer, b/o/x binary/octal/hex, s string.Percentage formatting expects a fraction
The % type multiplies by 100 and appends a percent sign. So it wants 0.75, not 75. Passing the number you already think of as a percentage gives you 7500.0%, which is the single most common formatting bug.
The rule: if the value came from a division - completed / total - it is already a fraction and :.1% is right. If you multiplied by 100 to "make it a percentage", use :.1f and write the % sign yourself.
0.75 with :.0%"75%" - correct. The value is a fraction and % scales it.0.75 with :.1%"75.0%" - one decimal place.75 with :.1%"7500.0%" - wrong. The value was already scaled and % scaled it again.75 with :.1f"75.0" - then add the sign yourself: f"{value:.1f}%".Watch out: Formatting changes only what is displayed. f"{price:.2f}" produces a new string; price itself keeps every digit it had.
Syntax and examples
name, age = "Chandu", 34
print("Hello " + name + ", you are " + str(age)) # concatenation
print("Hello %s, you are %d" % (name, age)) # % formatting
print("Hello {}, you are {}".format(name, age)) # str.format()
print(f"Hello {name}, you are {age}") # f-stringprice, quantity = 1000, 3
skills = ["Python", "React", "Node.js"]
user = {"name": "Chandu", "role": "Developer"}
print(f"Total: {price * quantity}") # arithmetic
print(f"Name: {name.upper()}") # a method call
print(f"Skills: {', '.join(skills)}") # a whole expression
print(f"Role: {user['role']}") # note the inner quotes differ
print(f"Status: {'Adult' if age >= 18 else 'Minor'}") # a conditionalprice = 1250000.5678
print(f"{price:.2f}") # 1250000.57 two decimal places
print(f"{price:,.2f}") # 1,250,000.57 with separators
print(f"{price:,.0f}") # 1,250,001 rounded to whole
progress = 0.8567
print(f"{progress:.1%}") # 85.7% % expects a fraction
invoice = 42
print(f"INV-{invoice:05}") # INV-00042 zero-padded to width 5
print(f"{255:b} {255:o} {255:x} {255:X}") # 11111111 377 ff FF
print(f"{1234.5678:.2e}") # 1.23e+03students = [("Chandu", 87.5), ("Ravi", 92.0), ("Priya", 78.25)]
print(f"{'Name':<12}{'Score':>8}")
print("-" * 20)
for name, score in students:
print(f"{name:<12}{score:>8.1f}")
# Name Score
# --------------------
# Chandu 87.5
# Ravi 92.0
# Priya 78.2
print(f"{' Report ':=^30}") # ========== Report ===========name, price, quantity = "Chandu", 100, 3
# = prints the expression as well as its value
print(f"{name=}") # name='Chandu'
print(f"{price * quantity=}") # price * quantity=300
print(f"{price * quantity=:,.2f}") # price * quantity=300.00
# !r shows the repr, so you can see quotes and whitespace
value = " spaced "
print(f"[{value}]") # [ spaced ]
print(f"{value!r}") # ' spaced ' <- the spaces are visiblefrom datetime import datetime
student, course = "Chandu", "Core Python"
completed, total = 6, 75
progress = completed / total
print(f"""Student: {student}
Course: {course}
Lessons: {completed}/{total}
Progress: {progress:.1%}
Printed: {datetime.now():%d-%m-%Y %H:%M}""")
# Double the braces to print one
print(f"{{Hello}}") # {Hello}
# Dynamic precision: the spec itself can contain a placeholder
places = 3
print(f"{3.14159:.{places}f}") # 3.142price, quantity, discount = 3499.50, 2, 0.10
# Work out the numbers...
subtotal = price * quantity
discount_amount = subtotal * discount
final = subtotal - discount_amount
# ...then decide how they look.
print(f"Product: Mechanical Keyboard")
print(f"Price: {price:>12,.2f}")
print(f"Quantity: {quantity:>12}")
print(f"Subtotal: {subtotal:>12,.2f}")
print(f"Discount: {discount:>12.0%}")
print(f"You save: {discount_amount:>12,.2f}")
print(f"Total: {final:>12,.2f}")Tip: Before Python 3.12 an f-string expression cannot contain a backslash, so f"{'\n'.join(x)}" is a syntax error. Put the separator in a variable first.
Format specs worth memorising
Nine shapes that cover almost everything you will format.
:.2fTwo decimal places. The everyday number format.
f"{3.14159:.2f}" # 3.14:,Thousands separators.
f"{1250000:,}" # 1,250,000:,.2fBoth together - the money format.
f"{1250.5:,.2f}" # 1,250.50:.1%Percentage from a fraction, one decimal place.
f"{0.857:.1%}" # 85.7%:05Zero-pad an integer to width 5.
f"{42:05}" # 00042:<12 :>12 :^12Left, right, centre within a width of 12.
f"{name:<12}":*^20Centre in 20 characters, padding with asterisks.
f"{'Hi':*^20}":.2eScientific notation.
f"{1234.5:.2e}" # 1.23e+03:b :o :x :XBinary, octal, lower and upper hex.
f"{255:x}" # fff-string extras and date codes
{value=}Prints the expression text as well as the value. Pure debugging gold.
f"{age=}" # age=34{value!r}Uses repr() instead of str(), so quotes and whitespace show.
f"{s!r}"{{ }}Doubled braces produce one literal brace.
f"{{x}}" # {x}{v:.{n}f}The spec can itself contain a placeholder, for precision decided at runtime.
f"{pi:.{n}f}"%Y-%m-%dDates format directly in an f-string - year, month, day.
f"{now:%Y-%m-%d}"%H:%M:%SHour, minute, second. %d-%m-%Y %H:%M is the common report stamp.
f"{now:%H:%M}"Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“print(f"{0.75:.1%}", f"{75:.1%}")”
“print(f"[{'ab':10}]", f"[{12:10}]")”
“x = 7; print(f"{x * 6=}")”
“n = 4; print(f"{3.14159265:.{n}f}")”
What usually goes wrong
Without it the braces are just characters, and Python prints them literally. No error, no clue - the output simply contains {name}.
✗ message = "Hello {name}" # Hello {name}✓ message = f"Hello {name}"The % format multiplies by 100. Give it 75 and you get 7500.0%. It wants the fraction that came out of the division.
✗ f"{75:.1%}" # 7500.0%✓ f"{0.75:.1%}" # 75.0%f"{price:.2f}" builds a string. The float keeps every digit it had, so a later calculation uses the full value - which is correct, and occasionally surprising.
✗ price = f"{price:.2f}" # price is now a string✓ display = f"{price:.2f}" # keep the number and the text apartAn f-string is for presentation. Once the expression has an and, an or, and a condition in it, nobody can read the sentence any more and no debugger can stop inside it.
✗ f"Status: {'OK' if a and b and not c else 'No'}"✓ status = "OK" if a and b and not c else "No"
f"Status: {status}"Before Python 3.12 the expression cannot use the same quote character that delimits the string. Use the other kind inside.
✗ f"Role: {user["role"]}"✓ f"Role: {user['role']}"It works until a value contains a quote, a newline, or a non-ASCII character, and then it produces something that is not valid JSON at all.
✗ body = f'{{"name": "{name}"}}'✓ import json
body = json.dumps({"name": name})Adjacent string literals are joined automatically, so a long message can be split across lines inside parentheses with no + and no backslashes.
✗ msg = f"Hello {name}, welcome to {course}. Progress {p:.0%}. Next: {nxt}."✓ msg = (
f"Hello {name}, welcome to {course}. "
f"Progress {p:.0%}. Next: {nxt}."
)Best practices
- Use f-strings for new code; reach for str.format() only when the template is separate from the values.
- Calculate above, format below. The f-string should decide how a value looks, not what it is.
- Keep the number and its display separate - never reassign a variable to its own formatted text.
- Use :,.2f for money, :.1% for a fraction you want as a percentage, and :0Nd for padded identifiers.
- Split long messages across adjacent f-string literals inside parentheses.
- Use f"{value=}" while debugging instead of writing the name twice.
- Use json.dumps() for JSON and the logging module’s own %s arguments for logs, not f-strings.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
With name = "Chandu" and age = 34, print "My name is Chandu and I am 34 years old." using an f-string.
Show solutionHide solution
name = "Chandu"
age = 34
print(f"My name is {name} and I am {age} years old.")Given price = 1200 and quantity = 3, print "Total: 3,600.00".
Show hintHide hint
Work out the total first, then use the money format.
Show solutionHide solution
price = 1200
quantity = 3
total = price * quantity
print(f"Total: {total:,.2f}") # Total: 3,600.00Given completed = 45 and total = 75, print "Progress: 60.0%".
Show hintHide hint
The division already gives you a fraction, which is what :.1% wants.
Show solutionHide solution
completed = 45
total = 75
progress = completed / total
print(f"Progress: {progress:.1%}") # Progress: 60.0%Display amount = 1256789.456 as "1,256,789.46".
Show solutionHide solution
amount = 1256789.456
print(f"{amount:,.2f}") # 1,256,789.46Turn invoice_id = 42 into "INV-00042".
Show solutionHide solution
invoice_id = 42
print(f"INV-{invoice_id:05}") # INV-00042Print "Python" three times in a width of 20 - left, right and centre aligned. Put square brackets around each so the padding is visible.
Show solutionHide solution
name = "Python"
print(f"[{name:<20}]") # [Python ]
print(f"[{name:>20}]") # [ Python]
print(f"[{name:^20}]") # [ Python ]With name = "Chandu" and age = 34, print name='Chandu' and age=34 without typing the variable names inside the text.
Show hintHide hint
There is a single character that does this.
Show solutionHide solution
name = "Chandu"
age = 34
print(f"{name=}") # name='Chandu'
print(f"{age=}") # age=34Print a three-row table of ("Chandu", 87.5), ("Ravi", 92.0), ("Priya", 78.25) with names left-aligned in 12 characters and scores right-aligned to one decimal place.
Show solutionHide solution
students = [("Chandu", 87.5), ("Ravi", 92.0), ("Priya", 78.25)]
print(f"{'Name':<12}{'Score':>8}")
print("-" * 20)
for name, score in students:
print(f"{name:<12}{score:>8.1f}")Student progress report
Produce a report card from raw numbers. Nothing displayed may be typed in by hand - every value on screen has to be calculated or formatted from the inputs.
- Start from a student, a course, completed and total lessons, and a score
- Work out the progress percentage and the lessons remaining yourself
- Show progress and score to one decimal place
- Use a single multiline f-string rather than many print calls
- Frame it with a rule, and align the labels so the values line up
student = "Chandu"
course = "Core Python"
completed_lessons = 6
total_lessons = 75
score = 87.5
# calculate, then formatShow one solutionHide solution
student = "Chandu"
course = "Core Python"
completed_lessons = 6
total_lessons = 75
score = 87.5
# Calculate first - the f-string only decides how these look.
progress = completed_lessons / total_lessons
remaining = total_lessons - completed_lessons
rule = "=" * 40
report = f"""{rule}
{'Student:':<20}{student}
{'Course:':<20}{course}
{'Progress:':<20}{progress:.1%}
{'Lessons remaining:':<20}{remaining}
{'Score:':<20}{score:.1f}%
{rule}"""
print(report)
# ========================================
# Student: Chandu
# Course: Core Python
# Progress: 8.0%
# Lessons remaining: 69
# Score: 87.5%
# ========================================Key points
- An f-string is a string with f before the quote; anything in braces is evaluated and inserted.
- The colon starts a format spec: [fill][align][sign][width][,][.precision][type].
- :,.2f is money, :.1% is a percentage, :05 pads an integer, :<12 and :>12 align in a column.
- The % type multiplies by 100, so it expects 0.75 rather than 75.
- In a width field, strings align left by default and numbers align right.
- Formatting produces a new string and never changes the value being formatted.
- f"{value=}" prints the expression and its value - the fastest debug print there is.
- !r shows the repr, which makes quotes and stray whitespace visible.
- Doubled braces {{ }} produce one literal brace.
- Adjacent f-string literals inside parentheses join automatically, which keeps long messages readable.
- Calculate above the f-string; keep logic out of the braces.
- Use json.dumps() for JSON rather than building it by hand.
Quick check before you move on
Interview questions
Why are f-strings faster than the alternatives?
They are compiled into bytecode that builds the string directly, rather than a runtime call that parses a template and looks up arguments. % and .format() both have to interpret the template each time the line runs; an f-string has already been parsed at compile time.
When would you still use str.format() or % formatting?
str.format() when the template and the values are separated - a message loaded from a config file or a translation catalogue - because an f-string is evaluated where it is written and cannot be stored as a template. % formatting when passing arguments to the logging module, where log.info("user %s", uid) defers the interpolation so it never happens if the level is filtered out.
Why is f"user {uid} failed" discouraged in a logging call?
Because it builds the string unconditionally, even when the log level means the message is thrown away. Passing the arguments separately lets logging skip the work entirely, which matters on a debug line inside a hot loop.
What are the security implications of formatting user input?
f-strings are evaluated at compile time from code you wrote, so they are safe. str.format() on an attacker-controlled template is not - a format string can reach attributes through {0.__class__}, which has been used to read secrets out of objects. Never call .format() on a template that came from outside.
How do you format a number to a precision decided at runtime?
Nest a placeholder inside the spec: f"{value:.{places}f}". The spec is itself formatted first, so places can be any expression. The same works for width.
What is the difference between str() and repr(), and how does that reach f-strings?
str() is for people and repr() is for developers - repr aims to show something unambiguous, usually valid Python. An f-string uses str() by default and !r switches to repr, which is why f"{s!r}" makes quotes and trailing whitespace visible when f"{s}" hides them.
Quiz
- 1.
What is an f-string?
- 2.
What does :,.2f mean?
- 3.
Does f"{price:.2f}" change price?
- 4.
Why is f"{75:.1%}" wrong when you want 75%?
- 5.
How do you print a literal brace?
- 6.
What does f"{name=}" produce?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned