f-strings and Formatting

Putting values inside strings, and the small format language that controls how each one is printed.

Overview

The prefix does the work

f"{name} scored {score}"

The f before the quote is what turns braces into slots. Without it you get the literal characters. Inside the braces goes any expression — a variable, a method call, arithmetic — evaluated at that point and inserted.

That is the whole idea, and it is why f-strings replaced everything before them: the value appears in the source where it appears in the output. Concatenation with + scatters the sentence across quotes and plus signs, and % and .format() push the values to the end, away from the slots they fill.

fstrings.py

fstrings.py Python 3
Output

                    

formatting_compare.py

formatting_compare.py Python 3
Output

                    

Worth knowing

The f prefix is required. Without it the braces are just braces.
:.2f fixes two decimal places, :, adds thousands separators, :>8 right-aligns in eight columns.
Formatting changes what is shown, never the value itself.
f"{x = }" prints both the expression and its value - the fastest debug print there is.

f-strings and Formatting: A Practical Guide

An f-string puts values inside a string where they will appear, and a small format language after each colon controls exactly how they are printed.

The format spec

After a colon comes a compact language for presentation:

  • :.2f — two decimal places
  • :, — thousands separators
  • :>8 — right-align in eight columns; < left, ^ centre
  • :03d — pad to three digits with zeros
  • :.1% — show as a percentage with one decimal

They combine: {price:>10.2f} right-aligns in ten columns with two decimals. That is how you get a table whose numbers line up without counting spaces by hand.

Formatting is not rounding

f"{value:.2f}" changes how the number is displayed. The variable is untouched, still carrying every digit it had. This matters when you print a rounded figure and then keep computing with the original: the printed total and the computed total can differ, and the program is not wrong — the display was never the value.

The debugging shortcut

f"{total = }"

prints total = 42 — the expression and its value. It works for any expression, so f"{len(items) = }" prints both the call and its result. It is the fastest way to instrument a function, and it removes the classic mistake of printing a label that no longer matches the variable beside it.

Which to use

Use f-strings. % formatting and .format() still work and you will meet them in older code, but they exist now mainly to be read, not written. The one place .format() still earns its keep is when the template is stored separately from the values — a message in a config file, for instance — because an f-string is evaluated where it is written.

Formatting a table without counting spaces

Alignment is where the format spec earns its keep. Left-align text, right-align numbers, and the columns line up whatever the data:

rows = [("apple", 3, 1.5), ("banana", 12, 0.25), ("cherry", 100, 12.0)]
print(f"{'item':<10}{'qty':>5}{'price':>10}")
for item, qty, price in rows:
    print(f"{item:<10}{qty:>5}{price:>10.2f}")

< left, > right, ^ centre, and the number after it is the field width. Numbers right-aligned with a fixed number of decimals is the combination you will use most, because it puts the decimal points under each other.

Dates, and why the same syntax works

The spec after the colon is handed to the object being formatted, so types can define their own:

from datetime import datetime
now = datetime(2026, 8, 20, 14, 30)
print(f"{now:%d %B %Y}")      # 20 August 2026
print(f"{now:%H:%M}")         # 14:30

That is not a special date feature of f-strings; it is datetime interpreting the spec it was given. The same mechanism is why :.2f means something to a float and nothing to a string.

Quotes inside the braces

Before Python 3.12 you could not reuse the outer quote character inside the expression:

f"{d['key']}"        # fine: single inside double
f"{d["key"]}"        # SyntaxError before 3.12, allowed after

Sticking to the older form costs nothing and works everywhere, which is the usual reason to keep doing it.

Escaping a brace

A literal brace is doubled:

print(f"{{not a placeholder}} but {2 + 2} is")
# {not a placeholder} but 4 is

This matters when you are generating JSON, CSS or anything else that uses braces, and it is the first thing to check when an f-string raises a confusing KeyError.

Why f-strings replaced everything before them

Python has had four ways to build a string with values in it, and the differences are about where the values sit relative to the text.

Concatenation with + scatters the sentence across quotes and plus signs, and requires str() around anything that is not already a string. Percent formatting moves the values to the end, so reading the sentence means jumping between the template and the tuple. str.format is the same shape with better syntax. An f-string puts each value exactly where it will appear in the output.

That single property is why the mistakes go away. You cannot pass the arguments in the wrong order, because there is no separate argument list. You cannot supply too few, because each slot contains its own expression. And you can read the line and know what it prints without looking anywhere else.

What belongs inside the braces

Any expression is legal, which means a great deal is possible and rather less is wise. A variable, an attribute, a dictionary lookup, a short method call or a small arithmetic expression all read fine. A nested comprehension or a long conditional does not: by the time the reader has parsed it they have lost the sentence it was embedded in.

The rule that holds up is that an f-string should still read as a sentence with holes in it. When the hole is bigger than the sentence, compute the value on the line above and give it a name.

Formatting and correctness

One habit worth keeping: format at the edge, never in the middle. Keep full precision in your variables and apply :.2f only where the value is printed or written out. Rounding early and then continuing to calculate is how totals stop matching the numbers above them, and it is a genuinely common source of small, hard-to-explain discrepancies in reports.

A worked example: a receipt

Alignment, decimals and a total, using nothing but the format spec:

items = [("apple", 3, 1.5), ("banana", 12, 0.25)]
total = sum(q * p for _, q, p in items)

print(f"{'item':<10}{'qty':>5}{'each':>8}{'cost':>9}")
for name, qty, price in items:
    print(f"{name:<10}{qty:>5}{price:>8.2f}{qty * price:>9.2f}")
print(f"{'total':<10}{'':>5}{'':>8}{total:>9.2f}")
item        qty    each     cost
apple         3    1.50     4.50
banana       12    0.25     3.00
total                       7.50

The header row uses the same widths as the data rows, which is what keeps the labels over their columns — and it works because a string can take an alignment and a width just as a number can. {'':>8} is an empty string padded to eight, which is how the total line skips columns without counting spaces.

Text is left-aligned and numbers right-aligned, which is not a style preference: right-aligning numbers puts the decimal points under each other, and that is what makes a column of figures readable at a glance.

Note that total is computed from the full-precision prices, not from the rounded strings. Formatting is applied only where the number is printed, which is the habit that stops a column of rounded values from failing to add up to the rounded total.

The three parts of a replacement field

Everything inside the braces follows one shape: {expression!conversion:format_spec}, and knowing the three slots explains several things that otherwise look like separate features.

The expression is any Python expression, evaluated at that point.

The conversion is !r, !s or !a, applied before formatting. !r calls repr instead of str, which is what puts quotes around a string and makes whitespace visible — invaluable in a debugging print, because ana and ana look identical without it.

The format spec is everything after the colon, and it is handed to the object itself. That is why :.2f means something to a float, %d %B %Y means something to a datetime, and neither means anything to the other.

The spec can contain its own braces, which is how a width computed at runtime gets in:

name = "ana"
w = 8
print(f"{name!r:>{w}}")
print(f"{3.14159:.{2}f}")
   'ana'
3.14

Nesting one level is allowed and is the standard way to align a table whose column widths are calculated from the data rather than written into the source.

Numbers as people read them

A handful of specs cover almost every number a person will look at.

print(f"{1234567:,}")
print(f"{0.256:.1%}")
print(f"{5:+d}")
1,234,567
25.6%
+5

, inserts thousands separators, and _ does the same with underscores. % multiplies by a hundred and appends the sign, which is the correct way to show a proportion — writing f"{x * 100:.1f}%" does the same thing with one more place to make a mistake. + forces a sign on positive numbers, which matters when the number is a change rather than a quantity.

Two further ones are worth having. :>10,.2f combines alignment, separators and decimals, which is the full treatment for a money column. And :g chooses between fixed and scientific notation based on magnitude, which is useful when the range of values is unknown and awful when it is not, because the column stops lining up.

For money specifically, remember that floats are not decimals. The formatting is honest about what it was given; if the arithmetic used floats, the rounding error was already there before the format spec saw it.

Where formatting belongs in a program

An f-string is a presentation decision, and presentation decisions are worth keeping in one layer rather than scattered through the code that does the work.

The symptom of getting this wrong is a function that returns a string where it should return a number. def total(items) -> str that hands back "£7.50" looks convenient at the call site and has thrown away everything else you might do with the value: adding it to another total, comparing it, storing it, converting the currency. The caller who needs the number now has to parse the string back, and the parse will eventually meet a thousands separator.

Keep values as values, and format at the point of output. A reporting function takes numbers and produces the string; the calculation returns numbers. This is the same boundary argument as converting input at the edge, run in the opposite direction: parse on the way in, compute in real types, format on the way out.

The second habit is to keep the template near the output rather than near the data. A dictionary of format specs per column, applied in the printing loop, is easy to change when the report changes. Specs scattered through the functions that computed each figure are not.

There is one honest exception: log messages and error text, where the string *is* the output and there is nothing downstream to preserve precision for. Even there, prefer logging.info("total %s", total) over an f-string, because the logger can then skip the formatting entirely when the message is filtered out.

Questions people ask

Can I use an f-string for a template stored elsewhere? No, it is evaluated where it is written. str.format on a stored template is the tool for that.

Is an f-string faster than concatenation? Yes, usually noticeably, because it does not build intermediate strings.

How do I put a quote inside? Use the other quote character, or — from Python 3.12 — the same one.

Can I have a backslash inside the braces? Not before Python 3.12. Compute the value on the line above instead.

What does f"{x=}" do? Prints the expression text, an equals sign, and the value's repr. It is the fastest debugging print there is.

Do f-strings work with multi-line strings? Yes, prefix a triple-quoted string with f and the braces work throughout.

Is it safe to put user input in one? For output, yes. Never build SQL or shell commands this way — use parameters, or the injection is on you.

Why does my f-string print braces literally? Because you doubled them, or because the f prefix is missing. Without the prefix the braces are ordinary characters.

Can I format a value whose width I only know at runtime? Yes, nest a field in the spec: f"{value:>{width}}".

What is the difference between !s and no conversion? Nothing, in practice — str is the default. !r is the one worth typing.

How do I show a number rounded but keep full precision? Format only where you print. The variable is never changed by a format spec, so the arithmetic that follows still uses every digit it had.

Can I align text and numbers in the same column? Yes, and it usually reads badly. Give the header the same alignment as the data if you want them to line up, or left-align both.

Recap in one screen

  • The f prefix turns braces into slots holding any expression, evaluated where it appears in the sentence.
  • After the colon is a spec handed to the object: .2f, ,, >8, % and %d %B %Y all work this way.
  • !r shows the repr, which is how you see quoting and stray whitespace.
  • Formatting changes the display, never the value — format at the edge and keep full precision in the arithmetic.
  • f"{value=}" prints the expression and its value together.

Check yourself

0 of 3

Answer without scrolling back up.

  1. What does `f"{2/3:.2f}"` produce?

  2. What is `{price:>10.2f}` doing?

  3. You write `"{name} scored"` with no f prefix. What prints?

Cheat sheet

f-strings and Formatting

An f-string puts values inside a string where they will appear, and a small format language after each colon controls exactly how they are printed.

PYTHON · vizlearn.in/python/f_strings_and_formatting.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.