Python String Formatting: How to Become a String Wizard with the Format Specification Mini-Language

Python provides fantastic string formatting options, but what if you need greater control over how values are presented? That’s where format specifiers come in. 

This article starts with a brief overview of the different string formatting approaches. We’ll then dive straight into some examples to whet your appetite for using Python’s Format Specification Mini-Language in your own projects.

But before all that—let’s play with string formatting yourself in the interactive Python shell:

Exercise: Create another variable tax and calculate the tax amount to be paid on your income (30%). Now, add both values income and tax in the string—by using the format specifier %s!

Don’t worry if you struggle with this exercise. After reading this tutorial, you won’t! Let’s learn everything you need to know to get started with string formatting in Python.

String Formatting Options

Python’s string formatting tools have evolved considerably over the years. 

The oldest approach is to use the % operator:

>>> number = 1 + 2
>>> 'The magic number is %s' % number
'The magic number is 3'


(The above code snippet already includes a kind of format specifier. More on that later…)

The str.format() method was then added:

>>> 'The magic number is {}'.format(number)
'The magic number is 3'

Most recently, formatted string literals (otherwise known as f-strings) were introduced. F-strings are easier to use and lead to cleaner code, because their syntax enables the value of an expression to be placed directly inside a string:

>>> f'The magic number is {number}'
'The magic number is 3'


Other options include creating template strings by importing the Template class from Python’s string module, or manually formatting strings (which we’ll touch on in the next section).

If this is all fairly new to you and some more detail would be helpful before moving on, an in-depth explanation of the main string formatting approaches can be found here.

Format Specifiers

With that quick summary out of the way, let’s move on to the real focus of this post – explaining how format specifiers can help you control the presentation of values in strings.

F-strings are the clearest and fastest approach to string formatting, so I will be using them to illustrate the use of format specifiers throughout the rest of this article. Please bear in mind though, that specifiers can also be used with the str.format() method. Also, strings using the old % operator actually require a kind of format specification – for example, in the %s example shown in the previous section the letter s is known as a conversion type and it indicates that the standard string representation of the object should be used. 

So, what exactly are format specifiers and what options do they provide?

Simply put, format specifiers allow you to tell Python how you would like expressions embedded in strings to be displayed.

Percentage Format and Other Types

For example, if you want a value to be displayed as a percentage you can specify that in the following way:

>>> asia_population = 4_647_000_000
>>> world_population = 7_807_000_000
>>> percent = asia_population / world_population
>>> f'Proportion of global population living in Asia: {percent:.0%}'
'Proportion of global population living in Asia: 60%'


What’s going on here? How has this formatting been achieved?

Well the first thing to note is the colon : directly after the variable percent embedded in the f-string. This colon tells Python that what follows is a format specifier which should be applied to that expression’s value.

The % symbol defines that the value should be treated as a percentage, and the .0 indicates the level of precision which should be used to display it. In this case the percentage has been rounded up to a whole number, but if .1 had been specified instead the value would have been rounded to one decimal place and displayed as 59.5%; using .2 would have resulted in 59.52% and so on.

If no format specifier had been included with the expression at all the value would have been displayed as 0.5952350454720123, which is far too precise!

(The % symbol applied in this context should not be confused with the % operator used in old-style string formatting syntax.)

Percentage is just the tip of the iceberg as far as type values are concerned, there are a range of other types that can be applied to integer and float values.

For example, you can display integers in binary, octal or hex formats using the b, o and x type values respectively:

>>> binary, octal, hexadecimal = [90, 90, 90]
>>> f'{binary:b} - {octal:o} - {hexadecimal:x}'
'1011010 - 132 - 5a'


For a full list of options see the link to the relevant area of the official Python documentation in the Further Reading section at the end of the article.

A close up of a reptile

Description automatically generated

Width Format, Alignment and Fill

Another handy format specification feature is the ability to define the minimum width that values should take up when they’re displayed in strings.

To illustrate how this works, if you were to print the elements of the list shown below in columns without format specification, you would get the following result:

>>> python, java, p_num, j_num = ["Python Users", "Java Users", 8.2, 7.5]
>>> print(f"|{python}|{java}|\n|{p_num}|{j_num}|")
|Python Users|Java Users|
|8.2|7.5|


Not great, but with the inclusion of some width values matters start to improve:

>>> print(f"|{python:16}|{java:16}|\n|{p_num:16}|{j_num:16}|")
|Python Users    |Java Users      |
|             8.2|             7.5|


As you can see, width is specified by adding a number after the colon.

The new output is better, but it seems a bit strange that the titles are aligned to the left while the numbers are aligned to the right. What could be causing this?

Well, it’s actually to do with Python’s default approach for different data types. String values are aligned to the left as standard, while numeric values are aligned to the right. (This might seem slightly odd, but it’s consistent with the approach taken by Microsoft Excel and other spreadsheet packages.)

Fortunately, you don’t have to settle for the default settings. If you want to change this behavior you can use one of the alignment options. For example, focusing on the first column only now for the sake of simplicity, if we want to align the number to the left this can be done by adding the < symbol before the p_num variable’s width value:

>>> print(f"|{python:16}|\n|{p_num:<16}|")
|Python Users    |
|8.2             |


And the reverse can just as easily be achieved by adding a > symbol in front of the width specifier associated with the title value:

>>> print(f"|{python:>16}|\n|{p_num:16}|")
|    Python Users|
|             8.2|


But what if you want the rows to be centered? Luckily, Python’s got you covered on that front too. All you need to do is use the ^ symbol instead:

>>> print(f"|{python:^16}|\n|{p_num:^16}|")
|  Python Users  |
|      8.2       |


Python’s default fill character is a space, and that’s what has so far been used when expanding the width of our values. We can use almost any character we like though. It just needs to be placed in front of the alignment option. For example, this is what the output looks like when an underscore is used to fill the additional space in the title row of our column:

>>> print(f"|{python:_^16}|\n|{p_num:^16}|")
|__Python Users__|
|      8.2       |


It’s worth noting that the same output can be achieved manually by using the str() function along with the appropriate string method (in this case str.center()):

>>> print("|", python.center(16, "_"), "|\n|", str(p_num).center(16), "|", sep="")
|__Python Users__|
|      8.2       |


But the f-string approach is much more succinct and considerably faster to evaluate at run time.

Of course, outputting data formatted into rows and columns is just one example of how specifying width, alignment and fill characters can be used.

Also, in reality if you are looking to output a table of information you aren’t likely to be using a single print() statement. You will probably have several rows and columns to display, which may be constructed with a loop or comprehension, perhaps using str.join() to insert separators etc.

However, regardless of the application, in most instances using f-strings with format specifiers instead of taking a manual approach will result in more readable and efficient code.

A picture containing camera

Description automatically generated

24-Hour Clock Display

As another example, let’s say we want to calculate what the time of day will be after a given number of hours and minutes has elapsed (starting at midnight):

>>> hours = 54
>>> minutes = 128
>>> quotient, minute = divmod(minutes, 60)
>>> hour = (hours + quotient) % 24
>>> f'{hour}:{minute}'
'8:8'


So far so good. Our program is correctly telling us that after 54 hours and 128 minute the time of day will be 8 minutes past 8 in the morning, but the problem is that it’s not very easy to read. Confusion could arise about whether it’s actually 8 o’clock in the morning or evening and having a single digit to represent the number of minutes just looks odd.

To fix this we need to insert leading zeros when the hour or minute value is a single digit, which can be achieved using something called sign-aware zero padding. This sounds pretty complicated, but in essence we just need to use a 0 instead of one of the alignment values we saw earlier when defining the f-string, along with a width value of 2:

>>> f'{hour:02}:{minute:02}'
'08:08'


Hey presto! The time is now in a clear 24-hour clock format. This approach will work perfectly for times with double-digit hours and minutes as well, because the width value is a maximum and the zero padding will not be used if the value of either expression occupies the entire space:

>>> hours = 47
>>> minutes = 59
...
>>> f'{hour:02}:{minute:02}'
'23:59'
A picture of stars in the sky

Description automatically generated

Grouping Options

The longer numbers get the harder they can be to read without thousand separators, and if you need to insert them this can be done using a grouping option:

>>> proxima_centauri = 40208000000000
>>> f'The closest star to our own is {proxima_centauri:,} km away.'
'The closest star to our own is 40,208,000,000,000 km away.'


You can also use an underscore as the separator if you prefer:

>>> f'The closest star to our own is {proxima_centauri:_} km away.'
'The closest star to our own is 40_208_000_000_000 km away.'


Putting It All Together

You probably won’t need to use a wide variety of format specification values with a single expression that often, but if you do want to put several together the order is important.

Staying with the astronomical theme, for demonstration purposes we’ll now show the distance between the Sun and Neptune in millions of kilometers:

>>> neptune = "Neptune"
>>> n_dist = 4_498_252_900 / 1_000_000
>>> print(f"|{neptune:^15}|\n|{n_dist:~^15,.1f}|")
|    Neptune    |
|~~~~4,498.3~~~~|


As you can see, reading from right to left we need to place the n_dist format specification values in the following order:

  1. Type  – f defines that the value should be displayed using fixed-point notation
  2. Precision – .1 indicates that a single decimal place should be used 
  3. Grouping – , denotes that a comma should be used as the thousand separator
  4. Width – 15 is set as the minimum number of characters
  5. Align – ^ defines that the value should be centered
  6. Fill – ~ indicates that a tilde should occupy any unused space

In general, format values that are not required can simply be omitted. However, if a fill value is specified without a corresponding alignment option a ValueError will be raised.

Final Thoughts and Further Reading

The examples shown in this article have been greatly simplified to demonstrate features in a straightforward way, but I hope they have provided some food for thought, enabling you to envisage ways that the Format Specification Mini-Language could be applied in real world projects.

Basic columns have been used to demonstrate aspects of format specification, and displaying tabular information as part of a Command Line Application is one example of the ways this kind of formatting could be employed. 

If you want to work with and display larger volumes of data in table format though, you would do well to check out the excellent tools provided by the pandas library, which you can read about in these Finxter articles.

Also, if you would like to see the full list of available format specification values they can be found in this section of the official Python documentation.

The best way to really get the hang of how format specifiers work is to do some experimenting with them yourself. Give it a try – I’m sure you’ll have some fun along the way!