Practical Guide to Python String Format Specifiers

Python 3.x introduced two key enhancements to string formatting:

  • The .format method and format specifiers with PEP-3101 (Python 3.0)
  • f-strings or literaly string interpolation with PEP-0498 (Python 3.6)

In this article I’m going to give specific examples on how format specifiers work using both .format and f-strings.

Format specifiers give you much greater control over how you want your output to look, but the grammar can feel foreign and confusing if you’re coming from python 2 where the extent of most string interpolation just involved using % symbols.

I expect you to have a basic working knowledge of how to use .format and f-strings, but here’s a refresher:

Using .format:

>>> name = "John"
>>> age = 15
>>> print("{} is age {}".format(name, age))
John is age 15
>>> print("{1} is age {0}".format(name, age))
15 is age John
>>> print("{name} is age {age}".format(name=name, age=age))
John is age 15

Using f-strings:

>>> name = "John"
>>> age = 15
>>> print(f"{name} is age {age}")
John is age 15

Now lets dig into format specifiers.

Format Specification

Here’s the grammar of a format specifier: [[fill]align][sign][#][0][width][grouping_option][.precision][type]

The official documentation on format specifiers does a great job explaining the meaning of each aspect of the format specifier grammar so I won’t repeat that here. I highly encourage you to read through the documentation on what each part of that grammar means first.

Lets start with 3 strings:

item_number = "103"
item_label = "Light Saber"
item_price = "256.128"

Every example will be accompanied by a use of both .format and f-strings. First, here’s a normal, default output without any format specification:

>>> print("{}, {}, {}".format(item_number, item_label, item_price))
103, Light Saber, 256.128
>>> print(f"{item_number}, {item_label}, {item_price}")
103, Light Saber, 256.128

Now I will progressively add format specifiers.

Again, here’s the full grammar of a format specifier: [[fill]align][sign][#][0][width][grouping_option][.precision][type].

Lets start with [[fill]align].

[[fill]align]

Lets make our item number take up a width of 50 characters:

>>> print("{:50}, {}, {}".format(item_number, item_label, item_price))
103                                               , Light Saber, 256.128
>>> print(f"{item_number:50}, {item_label}, {item_price}")
103                                               , Light Saber, 256.128

Now right adjust it:

>>> print("{:>50}, {}, {}".format(item_number, item_label, item_price))
                                               103, Light Saber, 256.128
>>> print(f"{item_number:>50}, {item_label}, {item_price}")
                                               103, Light Saber, 256.128

Don’t like blank spaces? Lets set x as the fill character (instead of spaces) and 50 as the width for item number. Make it left adjusted.

>>> print("{:x<50}, {}, {}".format(item_number, item_label, item_price))
103xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, Light Saber, 256.128
>>> print(f"{item_number:x<50}, {item_label}, {item_price}")
103xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, Light Saber, 256.128

Now make it right adjusted:

>>> print("{:x>50}, {}, {}".format(item_number, item_label, item_price))
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx103, Light Saber, 256.128
>>> print(f"{item_number:x>50}, {item_label}, {item_price}")
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx103, Light Saber, 256.128

Here’s tweaking both the fill and the width:

>>> print("{:j<10}, {}, {}".format(item_number, item_label, item_price))
103jjjjjjj, Light Saber, 256.128
>>> print(f"{item_number:j<10}, {item_label}, {item_price}")
103jjjjjjj, Light Saber, 256.128

What happens if you just give it a fill character and nothing else?

>>> print("{:x}, {}, {}".format(item_number, item_label, item_price))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Unknown format code 'x' for object of type 'str'
>>> print(f"{item_number:j}, {item_label}, {item_price}")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Unknown format code 'j' for object of type 'str'

Woops! When you give a fill character, you must provide at least an alignment symbol (< or >).

>>> print("{:x<}, {}, {}".format(item_number, item_label, item_price))
103, Light Saber, 256.128

However, you dont need to provide a width - if left out, the width will be whatever your content takes up. But wait, what’s the point of providing the adjustment character then? You’re right - there is really no point - it’s really just for the language parser to know what the heck x means. A nice example of a leaky abstraction.

In summary:

  • left / right adjustments are really only useful in the context of providing a fix width value
  • if you set a fill character, you can’t leave out the adjustment specification
  • the default fill is a space
  • the default adjustment is left
  • the default width is the width of the string

[sign]

Lets add signs to our item_number and item_price:

>>> print(f"{item_number:+}, {item_label}, {item_price:+}")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Sign not allowed in string format specifier

Woops! item_number and item_price are actually strings! You can only adds the sign symbol to numeric types. Lets re-declare those to be a number and float.

>>> item_number = 103
>>> item_price = 256.128
>>> print(f"{item_number:+}, {item_label}, {item_price:+}")
+103, Light Saber, +256.128

Great! Here’s the output if we negated item_number:

>>> print(f"{-item_number:+}, {item_label}, {item_price:+}")
-103, Light Saber, +256.128
>>> print("{:+}, {}, {:+}".format(-item_number, item_label, item_price))
-103, Light Saber, +256.128

What if we provide a negative sign?

>>> print(f"{-item_number:-}, {item_label}, {item_price:-}")
-103, Light Saber, 256.128
>>> print("{:-}, {}, {:-}".format(-item_number, item_label, item_price))
-103, Light Saber, 256.128

So if you do a negative sign, you get the default formatting behavior which is that only negative numbers have signs.

What happens now if we tried to apply our previous fill and alignment stuff to these numbers?

>>> print(f"{item_number:<50+}, {item_label}, {item_price:+}")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Unknown format code '+' for object of type 'int'

You can’t! But you can still align and fill (without providing sign):

>>> print(f"{item_number:<10}, {item_label}, {item_price:+}")
103       , Light Saber, +256.128

In summary:

  • you can only use signs on numeric types
  • you can’t use fill, alignment, and sign all at the same time. You can use fill and alignment on integers - which is probably sufficient for most use cases
  • by default if you don’t provide a sign, only negative numbers display with signs

[#]

Lets ignore the fill and align stuff moving forward - the rest of the specification (just like +) is specific to number types and, just like + will not work in conjunction with fill and align.

Add # to our signed formats:

>>> print("{:+#}, {}, {:+#}".format(-item_number, item_label, item_price))
-103, Light Saber, +256.128

Uh, didn’t do anything. That’s because you didn’t specify the type of integer you want formatted (it defaults to decimal).

Lets make it output binary:

>>> print("{:+#b}, {}, {:+#}".format(-item_number, item_label, item_price))
-0b1100111, Light Saber, +256.128

Hexadecimal:

>>> print("{:+#x}, {}, {:+#}".format(-item_number, item_label, item_price))
-0x67, Light Saber, +256.128

We haven’t covered [type] yet, but we’ll get to that later in more detail. For now, just know that for # to be useful, you need to also supply an integer format type.

[0][width]

I’m addressing [0] with [with] because they actually relate:

When no explicit alignment is given, preceding the width field by a zero (‘0’) character enables sign-aware zero-padding for numeric types. This is equivalent to a fill character of ‘0’ with an alignment type of ‘=’.

Here’s printing with 0 and without the 0 prefix to width:

>>> print(f"{item_number:+09}, {item_label}, {item_price:+9}")
+00000103, Light Saber,  +256.128
>>> print("{:+09}, {}, {:+9}".format(-item_number, item_label, item_price))
-00000103, Light Saber,  +256.128

As you can see, omitting the 0 will create a 9 character size output but without the 0 padding.

[grouping_option]

What if our item number was really big and we wanted some separators to make the magnitude more clear?

>>> print("{:+09_}, {}, {:+9}".format(-item_number, item_label, item_price))
-1_234_565_654, Light Saber,  +256.128
>>> print(f"{item_number:+09_}, {item_label}, {item_price:+9}")
+1_234_565_654, Light Saber,  +256.128

With commas!

>>> print("{:+09,}, {}, {:+9}".format(-item_number, item_label, item_price))
-1,234,565,654, Light Saber,  +256.128

[.precision]

Okay, now lets truncate our decimals for item_price into just a single precision:

>>> print("{:+09,}, {}, {:+9.1}".format(-item_number, item_label, item_price))
-1,234,565,654, Light Saber,    +3e+02

Oh weird - it’s giving my value in scientific notation. Turns out, when you specify a precision, the default display that gets used is exponent notation. Yes, it’s weird. That said, you can change the display back to fixed point by specifying a specific fixed point type as you’ll see next.

[type]

Lets add f for our item_price float interpolation to make it fixed point display.

>>> print("{:+09,}, {}, {:+9.1f}".format(-item_number, item_label, item_price))
-1,234,565,654, Light Saber,    +256.1

Hope that was helpful! I encourage you to go play around with the formatting options to really get familiar with it. It’s a very powerful formatting tool.