f-strings are Python's modern way to build strings out of values. They landed in Python 3.6 (PEP 498) and have been the default for new code since, because they're faster, shorter, and easier to read than the older % and .format() styles. This lesson covers what an f-string is, how the format spec inside the braces works, the debug shortcut {x=}, and the rules and pitfalls that govern their use.
An f-string is a string literal prefixed with f (or F). Anything inside {...} is treated as a Python expression, evaluated, converted to a string, and dropped into the result:
The rest of the string outside the braces is taken literally. The braces mark the holes where values get plugged in. No str() calls, no + operators, no juggling tuples of arguments. The variables sit where they appear in the sentence.
Compare that to the alternatives. With + concatenation:
Every non-string value needs str(). Every gap needs its own quoted space. The line gets noisy fast. With the older .format() method:
Better, but the placeholders sit in one place and the values in another, so reading requires bouncing between two locations. f-strings keep the values inline with the surrounding text.
The performance difference is measurable. The Python compiler turns each {...} in an f-string into a direct load-and-format instruction at parse time. There's no runtime parsing of a template string, no looking up positional arguments. For a single call the gap is microseconds, but in tight loops or hot code paths the f-string version is the fastest of the three formatting styles.
The contents of the braces don't have to be a plain variable name. They can be any Python expression that produces a value:
Method calls work too. So do attribute lookups, indexing, and conditional expressions:
A long arithmetic expression or a nested method call inside an f-string is harder to read than the same code split across two lines. Compute the value first, assign it to a variable with a clear name, then drop the variable into the f-string. Reserve inline expressions for short cases like indexing, simple arithmetic, or a single method call.
f-strings are evaluated every time the line runs. A debug-level log call like log.debug(f"state={huge_dict}") builds the full string even when debug logging is turned off. For logging, use the %-style form the logging module supports, which defers formatting until the message gets emitted.
Inside a placeholder, a colon introduces a format spec: a short mini-language that controls how the value gets rendered. The basic shape is:
The format spec sits after the colon and tells Python things like "round to two decimal places", "pad to ten characters wide", or "show this as hexadecimal". The full grammar is:
Every field is optional, and most format specs use only two or three at once. The fields appear in this order:
The fields used most often are width, precision, type, and grouping. The sections below cover each of them.
.N after the colon controls the number of digits after the decimal point. This is the most useful format spec for displaying money:
The f after the precision is the type code for fixed-point. Without f, Python uses a default that can switch between scientific notation and fixed-point depending on the magnitude. Spelling out f keeps the output predictable.
Rounding follows Python's standard banker's rounding (halves go toward the nearest even digit). For most money work this is fine. For stricter financial rounding rules, use the decimal module rather than the format spec.
A number on its own (or after a fill character and alignment marker) sets the minimum width. Strings default to left-aligned, numbers default to right-aligned:
The alignment markers are < for left, > for right, and ^ for center. The pipe characters in the example mark the column edges in the output. The minimum width is exactly that: a minimum. A longer value won't be truncated, it overflows:
The 26-character string doesn't fit in width 15, so the column overflows. To cap the width, combine it with precision, which truncates strings to that length. That combination appears below.
The character used for padding defaults to a space. You can change it by putting a character before the alignment marker:
*^20 means "fill with *, center, width 20". 0>5 means "fill with zero, right-align, width 5". The fill character must come paired with an alignment marker; fill alone is not allowed.
The leading 0 shortcut (before the width, without an alignment marker) is a special form for zero-padding numbers:
With the 0 form, the sign stays on the outside and zeros fill between the sign and the digits. This is the behavior wanted for fixed-width order IDs or version numbers.
The trailing letter in the format spec says how to interpret the value. Different types accept different codes.
| Code | For | Effect |
|---|---|---|
s | str | Plain string (default) |
d | int | Decimal integer |
b | int | Binary |
o | int | Octal |
x / X | int | Hex (lower / upper case) |
f / F | float | Fixed-point decimal |
e / E | float | Scientific notation |
% | float | Percentage (multiplies by 100, adds %) |
Several codes in action:
The % type handles discounts: it multiplies by 100 and appends the percent sign in one step. So 0.075 renders as 7.5%. Using the wrong type code for a value raises ValueError at runtime:
The .2f spec asks for a float, but name is a string. The error fires only when this line runs, not at parse time, so a stray bad spec can hide in code that rarely executes. Test format strings on real data.
A , or _ in the format spec adds a separator every three digits for decimal numbers (every four digits for binary and hex):
Commas are the standard for money. Underscores work for binary and hex output, where commas would look out of place. The grouping character goes right before the precision when both are used.
The two combine to give a fixed-width column that truncates anything too long:
Width 15, precision 15, left-aligned. Short names get padded with spaces, long names get cut off. For string types, precision is a maximum length, not a number of decimals. This keeps a column tidy when input lengths vary.
{x=}Added in Python 3.8, the = flag after an expression inside an f-string prints both the expression text and its value:
This form is built for debugging. Instead of typing the variable name twice (once as a label string, once as the value to print), the = does both. The exact text inside the braces, spaces and all, is preserved as the label:
The spaces around = in the first example are preserved in the output. The = flag combines with format specs too. The format spec goes after the =:
The debug form is shorter than scattering print("cart_total:", cart_total) lines through code while chasing a bug. It serves the same role as a console.log shortcut in other languages, and is useful for tracing values during development.
Triple-quoted f-strings work like normal triple-quoted strings: every newline and space between the quotes is preserved. This is how to build receipts, emails, or any block of text with values dropped in:
Each line of the template becomes a line in the output. The format specs (.2f here) work the same as in single-line f-strings. To keep Python source indented without indenting the output too, write the string starting at column zero or call textwrap.dedent() on the result.
Implicit string concatenation also works. When two string literals sit next to each other (separated only by whitespace), Python glues them at parse time into a single string. Mixing an f-string with a plain string this way is allowed:
Each part sits on its own line, no trailing + operators, and the parser stitches them together. This form keeps individual lines short.
Since { and } mark expression placeholders, writing a literal brace requires doubling them:
{{ becomes a single {, and }} becomes a single }. The escaping happens before Python parses what's inside the braces, so doubled braces never trigger expression evaluation.
A dict literal inside an f-string is a separate matter: a single {...} is an expression, not a literal brace. To write the text of a dict, either double the braces (which means no expressions inside), or use repr on a real dict.
Any expression goes, so method calls work fine:
This is useful for last-minute cleanup or formatting. If the chain of calls gets long, do the work on a separate line and put only the variable in the f-string. Readability beats compactness.
A format spec field can itself contain a placeholder, which is how width and precision get parameterized at runtime:
The inner {width} and {precision} evaluate first, their values get spliced into the format spec, then the outer formatting runs. This is the standard approach for building tables where column widths come from the data rather than from hardcoded constants.
Nesting a full f-string inside another f-string is also possible, though rarely needed. Each level of nesting needs a different quote style (or, in Python 3.12+, the same quote can be reused). The common case is the nested format spec, not nested whole f-strings.
Several rules govern what can go inside an f-string expression.
In Python 3.6 through 3.11, the expression inside {...} couldn't contain a backslash:
The fix on older versions is to put the special character in a variable first:
Python 3.12 lifted this restriction (PEP 701), so on 3.12 and later f"{'\n'.join(products)}" works directly. Code targeting older versions still needs the variable workaround.
# Comments Inside the BracesThe hash character starts a comment in normal Python, but inside an f-string expression it isn't allowed. There's no way to add an inline comment to the expression part:
The # and everything after it inside the literal is text in the output, not a Python comment. That's almost always the intended behavior. A # inside the braces is also disallowed: Python 3.11 and earlier raise a SyntaxError. Comments belong on the lines around the f-string, not inside it.
Before Python 3.12, the expression inside an f-string couldn't use the same kind of quote that wrapped the f-string:
The parser hits the inner " and treats the f-string as ended. The fix on older versions is to use the other quote style for the inner string:
Single quotes outside, double inside, or vice versa. Python 3.12 lifted this restriction too, but code that needs to run across versions should follow the older rule.
Before applying the format spec, the value can be routed through one of three conversion functions:
| Flag | Calls | Use for |
|---|---|---|
!s | str(value) | Default, rarely written |
!r | repr(value) | Debug output with quotes around strings |
!a | ascii(value) | Like !r but escapes non-ASCII chars |
!r is the practical one. When a log line says Order id was order_123, the reader can't tell whether order_123 is the string "order_123" or some other value that prints the same. !r puts the quotes around strings (and brackets around lists, braces around dicts) so the type is unambiguous.
The flag goes before the colon when combined with a format spec:
!r first wraps Mouse in quotes (giving 'Mouse', 7 characters), then the format spec right-aligns the result to width 15.
A small program that builds a receipt using width, alignment, precision, and grouping together:
The column widths come from variables that the format specs reference through nested placeholders, so changing one constant at the top adjusts every column. Names get truncated to the column width with .{NAME_W}, so a long product title can't overflow the layout. Prices are right-aligned, comma-grouped, and pinned to two decimals.
10 quizzes