Formatting output is exactly what f-strings are for, and they are the fastest and most readable way to build aligned, precise text in Python. They replaced two older styles that were harder to read and easier to get wrong. They were introduced in Python 3.6 and have been improved in every release since, most usefully in 3.12, which allowed the same quote character inside an expression and multi-line f-strings. The format specification language deserves proper coverage, because that is where f-strings stop being convenient and start being powerful.
What is an f-string?
Prefix a string literal with the letter f and any expression inside braces is evaluated and converted to text. That is the whole idea. Everything else is detail about how the result is laid out.
name = 'Ada'
score = 92.4567
print(f'Hello, {name}!')
print(f'{name} scored {score} points.')
print(f'{name!r} is a {type(name).__name__}')
print(f'{name=}')
print(f'{score=:.2f}')
print(f'{{literal braces}} next to {name}')
Four of those lines carry extra features. The !r suffix applies the repr() conversion, so quotes appear around a string, which is useful when you are debugging. The = specifier prints the expression and its value together, which is a debugging aid you will use constantly. Doubling the braces prints literal braces, which matters when your text contains JSON or CSS. And f'{score=:.2f}' combines the two, giving you score=92.46 without repeating the variable name.
The older alternatives still exist. '{}'.format(value) uses numbered or named placeholders, and '%s' % value is the printf style from the previous lesson. Both are harder to read and slower to execute, because f-strings are compiled into a faster instruction sequence rather than parsed at runtime. Use them only when you are reading old code.
The format specification itself has a documented grammar, and it is worth knowing the shape even if you only use half of it. In order, it accepts a fill character, an alignment, a sign option, a width, a thousands separator, a precision and a type character. Filling and alignment are the two you will use most often, because they are what turn a list of values into a readable table.
How do you control alignment and width?
After the expression comes a colon, and after the colon comes a format specification. The most useful part of it is a width with an alignment character: < for left, > for right, and ^ for centred. A number pads with spaces, a string pads with spaces too. You can also supply a fill character and a total width.
rows = [('keyboard', 129.99), ('monitor', 249.5), ('cable', 9.99)]
print(f'{"item":<12}{"price":>10}')
print('-' * 22)
for item, price in rows:
print(f'{item:<12}{price:>10.2f}')
print()
print(f'{"centre":^22}')
print(f'{"right":>22}')
print(f'{"left":<22}')
print(f'{"filled":*^22}')
Two details make this practical. A width applies to the formatted result, so {price:>10.2f} means "two decimal places, right aligned in ten characters". And the precision applies only to numbers; for a string, .2 would mean something entirely different, so keep the two apart in your head.
How do you format numbers?
The type character at the end of the specification decides the presentation. A comma adds thousands separators, % turns a fraction into a percentage, and the letters b, o, x and X produce binary, octal, hexadecimal and uppercase hexadecimal.
value = 1234567.891
print(f'{value:,.2f}')
print(f'{value:+.2f}')
print(f'{value:.3e}')
print(f'{value:.4g}')
print(f'{0.256:.1%}')
print(f'{255:b} {255:o} {255:x} {255:X} {255:#x}')
print(f'{42:>10,}')
The comma and the plus sign can be combined, which is what makes signed currency trivial. The g format is worth knowing because it switches between fixed and scientific notation depending on the size of the number, which is exactly what you want in a report where values range from pennies to millions.
One caution: the formatting is a display concern, not a rounding concern. f'{value:,.2f}' rounds for display, but the underlying float is unchanged, so adding numbers that were only ever rounded for display is how rounding errors get into a total. Keep the precise values in the data and format at the edge.
Which f-string tricks save you time?
Beyond the format specification there are a handful of details worth knowing. Date and time objects understand their own format codes, and they can be used directly inside an f-string.
from datetime import date, datetime
payload = {'user': 'ada', 'roles': ['admin', 'dev']}
print(f'plain -> {payload}')
print(f'repr -> {payload!r}')
when = datetime(2026, 10, 27, 14, 30)
print(f'short -> {when:%Y-%m-%d %H:%M}')
print(f'long -> {when:%B %d, %Y}')
print(f'date -> {date(2026, 1, 5):%d %b %Y}')
Because Python 3.12, an f-string can span several lines and can use the same quote character inside an expression as the one that delimits the string. That removes a pile of workarounds that used to be necessary, such as assigning a dictionary lookup to a variable first.
class Temperature:
"""A reading in Celsius that knows how to format itself."""
def __init__(self, celsius):
self.celsius = celsius
def __format__(self, spec):
if not spec:
spec = '.1f'
return f'{self.celsius:{spec}} C'
reading = Temperature(21.456)
print(f'{reading}')
print(f'{reading:.2f}')
name = 'Ada'
items = ['keyboard', 'monitor']
report = f'''
Report for {name}
Items: {", ".join(items)}
'''
print(report)
The __format__ method is how an object opts into the f-string protocol. The built-in format() function calls it too, so a class that implements it can be formatted anywhere a number can. You will meet the same idea in the standard library, where dates, decimals and durations all provide their own formatting behaviour.
Your turn: build a receipt formatter
- Write a script with a list of
(item, quantity, unit_price)triples and print a receipt: the item name left-aligned in 20 characters, the quantity right-aligned in 4, and the line total right-aligned in 10 with two decimal places. - Print a separator line of the right width, then a total line using an f-string with
=so it reads liketotal=57.48. - Add a percentage column showing each item's share of the bill, using the
%format, and make sure the percentages line up in the same column as the prices. - Print the payment date using a
datetimeinside an f-string in two formats:%Y-%m-%dand%d %B %Y. - Finally, give one of your own classes a
__format__method, and format it in an f-string with and without a specification. Confirm that the two outputs differ only in the number of decimal places.
Formatting quick reference
- Put expressions in braces inside an f-string; prefix the literal with
f. - After a colon comes the specification:
<,>or^for alignment, a width, a precision for numbers, and a type character. - Use
,for thousands separators,%for percentages, andb,o,xfor other bases. =prints the expression with its value, and!ror!schoose the conversion.- Escape literal braces as
{{and}}, and remember that Python 3.12 allows nested same-quote expressions and multi-line f-strings.