Topic 3.3
Formatting Text: f-strings in Depth
In one line
An f-string is a string with {} holes that Python fills with values, and a small format-spec language after a colon controls width, alignment, number style, percentages and dates. This topic covers the whole language, plus the older str.format and % styles you will meet in other people's code.
Think of it like this
A printed form with blank boxes. "Name: ____ Price: ____". The words are fixed; you fill the boxes in. You can also say how to fill each box: "print the price with two decimals, right-aligned in 10 spaces". An f-string is that form: fixed text with {} boxes, plus (after a colon) instructions for how to write each value.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- f-string
- A string that starts with
fand contains{}holes filled with the values of expressions. - Placeholder
- One
{...}hole in a formatted string. - Format spec
- The mini-language after
:that says how to write a value: width, alignment, decimals and so on. - Width
- The minimum number of characters a value should take up. Shorter values are padded.
- Alignment
- Where a value sits inside its width: left
<, right>or centre^. - Precision
- For decimals, how many digits come after the point (
.2means two). - Conversion
!r,!sor!a: turn a value into text withrepr,strorasciibefore formatting.- Escape a brace
- Write
{{or}}to print a real brace instead of starting a placeholder. - Template string
- A
t"..."string (Python 3.14) that keeps the fixed text and the values as separate parts.
Step by step
01Holes hold expressions
Everything inside {} is real Python code, evaluated when the line runs. You can calculate, call methods or use a conditional expression right there. Keep it short; if the expression is hard to read, calculate it on the line above and put the result in a variable.
price = 250
qty = 3
name = "tea"
print(f"{qty} x {name.upper()} = {price * qty}")
print(f"Free delivery: {'yes' if price * qty > 500 else 'no'}")
print(f"Braces: {{literal}} and {qty}")02Width, alignment and fill
Put a colon, then optionally a fill character, then <, > or ^, then the width. Without an alignment, strings sit on the left and numbers on the right. That is why tables of text and numbers line up with almost no effort.
name = "Asha"
print(f"[{name:<8}]") # [Asha ]
print(f"[{name:>8}]") # [ Asha]
print(f"[{name:^8}]") # [ Asha ]
print(f"[{name:*^8}]") # [**Asha**]03Numbers: decimals, commas, percent, padding
.2f fixes the digits after the point (and rounds). , groups thousands. % multiplies by 100 and adds the percent sign, so 0.256 becomes 25.6%. 05d pads a whole number with zeros to width 5 (useful for ids and file names). +d always prints a sign.
Formatting rounds the number for display; the variable itself keeps its full value. Rounding uses the real binary value of the float, which is why f"{2.675:.2f}" shows 2.67 (Topic 1.3).
price = 1234.5
ratio = 0.256
count = 42
print(f"{price:,.2f}") # 1,234.50
print(f"{ratio:.1%}") # 25.6%
print(f"{count:05d}") # 00042
print(f"{count:+d}") # +4204Other bases and scientific notation
The type letters b, o, x show a whole number in binary, octal and hexadecimal; # adds the matching prefix. e writes scientific notation, handy for very large and very small numbers. These show up when you read bit flags, colours (#ff8800) and memory addresses.
n = 42
print(f"{n:b} {n:o} {n:x} {n:#x}") # 101010 52 2a 0x2a
print(f"{123456.789:.3e}") # 1.235e+0505Conversions and the = debugging form
!r is for debugging: it shows what the value really is, with quotes. = goes one step further and prints the expression as well, so a debugging line needs no label that can drift out of sync with the code.
name = "Asha"
count = 42
print(f"{name!r} vs {name!s}") # 'Asha' vs Asha
print(f"{count = }") # count = 42
print(f"{count * 2 = }") # count * 2 = 8406Nested fields: the width comes from a variable
A format spec may contain placeholders of its own. This lets the layout be decided at run time, for instance to size a column to the longest word. The inner braces are filled first, then the result is used as the spec.
width = 12
precision = 3
print(f"[{3.14159265:{width}.{precision}f}]") # [ 3.142]07Dates have their own spec
A value decides what its spec means. Numbers use the number language; date and datetime objects use strftime codes: %Y year, %m month number, %d day, %B month name, %A weekday name, %H:%M time. Phase 6 covers the datetime module; for now, treat date(2026, 10, 9) as "the ninth of October 2026". The first line, from datetime import date, borrows a ready-made tool that Python already ships with (Phase 6 explains import).
from datetime import date
d = date(2026, 10, 9)
print(f"{d:%d %B %Y}") # 09 October 2026
print(f"{d:%A}") # Friday08The older styles: format and %
str.format is useful when the template is data: it can be loaded from a file or a setting and filled later. % formatting is still the style the logging module uses. Both are fully supported; neither is deprecated. You will read plenty of code that uses them.
One safety rule for str.format: never fill a template that came from an untrusted user. A template can reach into objects ({0.__class__}) and reveal things you did not intend to show. Fixed templates written by you are safe.
print("{} is {} years old".format("Asha", 30))
print("{name:>8}|".format(name="Asha"))
print("%s has %d items costing %.2f" % ("Asha", 3, 12.5))09Python 3.12 quote reuse, and a peek at 3.14
Before Python 3.12, an f-string could not use its own quote type inside the braces, so people alternated " and '. From 3.12, f-strings are parsed by the normal Python grammar, so inner expressions may reuse the same quote, span several lines and contain backslashes.
Python 3.14 adds template strings: t"Hello {name}" does not make a str. It makes a Template object that holds the fixed pieces and the values separately, so a library can treat the values safely (escape them for HTML or SQL) before joining. It is shown below as a read-only preview, because the browser runs Python 3.13.
Try it yourself
- 1
Build a receipt
Use three
printlines with the table example as a model. Add a fourth item. Change the column widths so the longest name fits. Try^for the heading. - 2
Debug with =
Set
a = 7andb = 3. Printf"{a = }, {b = }, {a / b = :.3f}". Note that=can be followed by a spec. Then add!ron a string variable. - 3
Make the width dynamic
Set
w = 20. Print the word"Hectal"centred in a width ofwusingf"|{'Hectal':^{w}}|". Changewand re-run.
Code & diagrams
Change the width, the fill character or the digits and watch the line change.
Expected output
[Asha ] [ Asha] [ Asha ] [**Asha**]
price: 1,234.50
price: [ 1234.5]
ratio: 25.6%
count: 00042 +42 101010 2a 0x2a 52
big: 1,234,567,890 1_234_567_890
sci: 1.235e+05
'Asha' vs Asha
count = 42
count * 2 = 84
[ 3.142]Expected output
09 October 2026
Friday
2026-10-09
--- str.format ---
Asha is 30 years old
hello world
Asha|
--- % style ---
Asha has 3 items costing 12.50
42|42 |
100% sureStrings are left-aligned by default and numbers right-aligned, so a table lines up almost for free.
Expected output
Item Qty Price
-------------------------
Pen 3 12.50
Notebook 12 1,250.00
Bag 1 999.99This is a syntax error before Python 3.12, because the inner quotes end the f-string early.
Expected output
You have 3 items
letters: a, b, c
two lines: a
b
repeat: hahahaNot runnable here: the browser runs Python 3.13. A `t` string builds a `Template`, not a `str`. Code or a library then decides how to combine the parts, for example by escaping each value.
from string.templatelib import Interpolation
name = "<script>"
tpl = t"Hello {name}!"
print(tpl.strings) # the fixed pieces
for part in tpl:
if isinstance(part, Interpolation):
print("value:", part.value) # the live value, not yet turned into textOutput
('Hello ', '!')
value: <script>Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a number spec on a string
Run name = "Asha" and print(f"{name:.2f}").
name = "Asha"
print(f"{name:.2f}")Myth vs fact
Myth
f-strings are only for putting variable names in text.
Fact
Each {} holds any expression, and the format spec after the colon is a full mini-language for widths, decimals, dates and more.
Myth
% and .format are deprecated.
Fact
Both are fully supported and widely used (logging still uses % style). f-strings are usually the best choice for new code.
Myth
Formatting with .2f rounds the variable.
Fact
It only changes how the number is written. The variable keeps its full value.
Myth
You can build an f-string in a variable and fill it in later.
Fact
An f-string is evaluated immediately. A template that is filled later needs str.format, or a template string in Python 3.14.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
f-strings compile to bytecode that builds the pieces directly (
FORMAT_VALUEandBUILD_STRINGin older versions), without calling a method by name. They are normally faster than%and.format, but the difference rarely matters. Choose for readability. For logging, uselogger.info("x=%s", x): the message is only built if that log level is enabled (Topic 7.9). - ▸
Because a format spec is passed to
format(value, spec), your own classes can define__format__and support specs like{money:.2f}or{point:x}(Topic 8.7).datetimedoes exactly this withstrftimecodes. - ▸
Python 3.12 (PEP 701) moved f-string parsing into the regular grammar. Besides quote reuse, it allows comments and line breaks inside the braces. Template strings (PEP 750, Python 3.14) go the other way: they refuse to produce a
str, so that code which builds HTML, SQL or shell commands can receive the values as separate parts and escape them. This is the design answer to injection attacks that come from gluing untrusted text into a string. - ▸
Money and rounding.
f"{x:.2f}"rounds the exact binary value of a float, sof"{2.675:.2f}"is2.67(the stored number is slightly below 2.675), whileDecimal("2.675")holds the decimal digits exactly and formats as2.68(ties go to the even digit). Do not use floats for currency: keep integer cents orDecimaland format only at the edge. Two small spec details worth knowing:z(Python 3.11) turns a negative zero such as-0.0001into0.00instead of-0.00, and_groups binary, octal and hex digits in fours (f"{255:_b}"is1111_1111). Thentype uses the current locale for separators, which is rarely what a web service wants.
Remember this
- 1
An f-string starts with
fbefore the opening quote. Inside, each{...}holds a Python expression (a piece of code that gives a value), which Python works out and converts to text:f"{price * qty}",f"{name.upper()}",f"{'yes' if ok else 'no'}". Anything outside braces is copied as it is. To print a literal brace, double it:{{and}}. An f-string is evaluated at the moment it runs, so it cannot be stored and filled later; for that you usestr.format(below) or, from Python 3.14, template strings. - 2
After the expression you can add
:and a format spec. Its general shape is[[fill]align][sign][z][#][0][width][grouping][.precision][type], and every part is optional. Width is the minimum number of characters; alignment is<left,>right,^centre; a fill character before the alignment replaces the default space ({name:*^10}). Strings default to left-aligned and numbers to right-aligned. Do not try to memorise that shape. The examples below build it up one piece at a time. - 3
For numbers, precision (
.2) means "two digits after the decimal point" forf, andtypepicks the style:dwhole number,ffixed decimals,escientific,%multiply by 100 and add a percent sign,bbinary (base 2, only 0 and 1),ooctal (base 8),xorXhexadecimal (base 16, digits 0 to 9 and a to f).,or_adds thousands separators,+always shows the sign,0pads with zeros ({n:05d}), and#adds the0xprefix. So{1234.5:,.2f}is1,234.50and{0.256:.1%}is25.6%. - 4
Three conversions come before the colon:
!rusesrepr()(shows quotes and escapes),!susesstr(), and!ausesascii(). And the **=specifier** (Python 3.8) prints the expression text as well as its value:f"{count = }"givescount = 42, and works with any expression. It is the fastest debugging tool in Python. - 5
Format specs can contain their own braces, which lets the width or precision come from variables:
f"{value:{width}.{precision}f}". Objects choose what a spec means: dates accept thestrftimecodes, sof"{d:%d %B %Y}"prints09 October 2026. Under the hood Python callsformat(value, spec), which calls the value's__format__method (one of the special methods of Topic 8.7). - 6
Two older styles still appear everywhere.
"{} is {}".format(a, b)uses the same spec language with positions ({0}) or names ({name}) and works on strings that are stored and filled later. The oldest,"%s has %d items" % (name, n), comes from the C language; Python'sloggingmodule still uses it (Topic 7.9). For new code, prefer f-strings. - 7
Two newer changes. Since Python 3.12, f-strings may reuse the same quote type inside braces (
f"{d["name"]}"), and since Python 3.14, atprefix makes a template string, which keeps the pieces separate instead of building text (see the preview below).
Explain it without notes
What does f"{x:>10.2f}" do, part by part?
How do = and !r help when debugging?
Why is it dangerous to call .format on a template that comes from a user?
When would you still choose str.format or % over an f-string?
Practice
Print the item name Tea left-aligned in 8 characters and the price 1234.5 right-aligned in 12 characters with a thousands separator and two decimals.
With done = 7 and total = 8, print 7/8 = 87.5%.
With x = 5, print x = 5, x ** 2 = 25 using the = specifier, without typing the names yourself.
Trade-offs
- ↔
Readability against cleverness. A long chain of nested specs is hard to read. Past one or two nested fields, calculate pieces into named variables first.
- ↔
Display against data. Format only at the edge, when you print or save. Keep numbers as numbers in the middle of your program; converting to text early (
"1,234.50") makes later maths impossible.
Done when you can
Done when you can read a format spec and say what each part does.
Done when you can align text and numbers in a table with width and alignment.
Done when you can show money, percentages, zero-padded ids and hex with the right spec.
Done when you can use
=and!rto debug.Done when you can read
.formatand%code written by others.Done when you can explain what quote reuse (Python 3.12) and template strings (Python 3.14) add.