Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~28 min· topic 3 of 7

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 f and 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 (.2 means two).
Conversion
!r, !s or !a: turn a value into text with repr, str or ascii before 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.

main.pywhole filepython
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.

main.pywhole filepython
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).

main.pywhole filepython
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}")        # +42

04Other 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.

main.pywhole filepython
n = 42
print(f"{n:b} {n:o} {n:x} {n:#x}")     # 101010 52 2a 0x2a
print(f"{123456.789:.3e}")              # 1.235e+05

05Conversions 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.

main.pywhole filepython
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 = 84

06Nested 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.

main.pywhole filepython
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).

main.pywhole filepython
from datetime import date

d = date(2026, 10, 9)
print(f"{d:%d %B %Y}")     # 09 October 2026
print(f"{d:%A}")           # Friday

08The 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.

main.pywhole filepython
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. 1

    Build a receipt

    Use three print lines 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. 2

    Debug with =

    Set a = 7 and b = 3. Print f"{a = }, {b = }, {a / b = :.3f}". Note that = can be followed by a spec. Then add !r on a string variable.

  3. 3

    Make the width dynamic

    Set w = 20. Print the word "Hectal" centred in a width of w using f"|{'Hectal':^{w}}|". Change w and re-run.

Code & diagrams

The format-spec toolbox New tab

Change the width, the fill character or the digits and watch the line change.

Sign in to run this example in your browser.

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]
Dates and the older styles New tab
Sign in to run this example in your browser.

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% sure
A neat table New tab

Strings are left-aligned by default and numbers right-aligned, so a table lines up almost for free.

Sign in to run this example in your browser.

Expected output

Item        Qty     Price
-------------------------
Pen           3     12.50
Notebook     12  1,250.00
Bag           1    999.99
Quote reuse inside an f-string Python 3.12+ New tab

This is a syntax error before Python 3.12, because the inner quotes end the f-string early.

Sign in to run this example in your browser.

Expected output

You have 3 items
letters: a, b, c
two lines: a
b
repeat: hahaha
A template string (Python 3.14 preview) Python 3.14+python

Not 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 text

Output

('Hello ', '!')
value: <script>
Anatomy of a format specdiagram
Rendering diagram…

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}").

main.pywhole filepython
name = "Asha"
print(f"{name:.2f}")
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(f"{name:.2f}")
^^^^^^^^^^
ValueError: Unknown format code 'f' for object of type 'str'

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_VALUE and BUILD_STRING in 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, use logger.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). datetime does exactly this with strftime codes.

  • ▸

    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, so f"{2.675:.2f}" is 2.67 (the stored number is slightly below 2.675), while Decimal("2.675") holds the decimal digits exactly and formats as 2.68 (ties go to the even digit). Do not use floats for currency: keep integer cents or Decimal and format only at the edge. Two small spec details worth knowing: z (Python 3.11) turns a negative zero such as -0.0001 into 0.00 instead of -0.00, and _ groups binary, octal and hex digits in fours (f"{255:_b}" is 1111_1111). The n type uses the current locale for separators, which is rarely what a web service wants.

Remember this

  1. 1

    An f-string starts with f before 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 use str.format (below) or, from Python 3.14, template strings.

  2. 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. 3

    For numbers, precision (.2) means "two digits after the decimal point" for f, and type picks the style: d whole number, f fixed decimals, e scientific, % multiply by 100 and add a percent sign, b binary (base 2, only 0 and 1), o octal (base 8), x or X hexadecimal (base 16, digits 0 to 9 and a to f). , or _ adds thousands separators, + always shows the sign, 0 pads with zeros ({n:05d}), and # adds the 0x prefix. So {1234.5:,.2f} is 1,234.50 and {0.256:.1%} is 25.6%.

  4. 4

    Three conversions come before the colon: !r uses repr() (shows quotes and escapes), !s uses str(), and !a uses ascii(). And the **= specifier** (Python 3.8) prints the expression text as well as its value: f"{count = }" gives count = 42, and works with any expression. It is the fastest debugging tool in Python.

  5. 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 the strftime codes, so f"{d:%d %B %Y}" prints 09 October 2026. Under the hood Python calls format(value, spec), which calls the value's __format__ method (one of the special methods of Topic 8.7).

  6. 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's logging module still uses it (Topic 7.9). For new code, prefer f-strings.

  7. 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, a t prefix makes a template string, which keeps the pieces separate instead of building text (see the preview below).

Explain it without notes

01

What does f"{x:>10.2f}" do, part by part?

02

How do = and !r help when debugging?

03

Why is it dangerous to call .format on a template that comes from a user?

04

When would you still choose str.format or % over an f-string?

Practice

01

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.

02

With done = 7 and total = 8, print 7/8 = 87.5%.

03

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 !r to debug.

  • Done when you can read .format and % code written by others.

  • Done when you can explain what quote reuse (Python 3.12) and template strings (Python 3.14) add.