The Curious Little Shop at The End of My Street • Python's f-strings
Many people's favourite Python feature: f-strings
There's a curious little shop at the end of my street. It's rather unremarkable from the outside and easy to miss. When you walk inside, you don't quite know what to make of the shop. There's an eclectic mix of trinkets hanging on the wall. There are quite a few of them, but the shop doesn't feel too cluttered. The smell is equally peculiar—a mix of freshly ground coffee and new plastic.
You spot the counter amidst all the stuff. And behind the counter, you see an old-fashioned blackboard with the price list written in flowing, cursive handwriting.
That's quite a range of things for sale.
Let's replicate the text on this sign and explore Python's f-strings along the way.
A quick author’s note: I’m in the process of releasing my first video course at The Python Coding Place called A Python Tale. More than half of it is already available. This is a beginner’s course, so unlikely to be relevant to many readers of The Stack—you’re likely beyond beginners’ stage. But if you know anyone who’s just starting, they can start this video course, A Python Tale, which is completely free.
This is the first of many courses on The Python Coding Place which will cover all levels.
What's Wrong With Plain Old Strings?
I'll go through all the step-by-step details soon. But let's jump a bit ahead. Why can't we just use a simple string to print out the item's name and price?
The simplest option, which you'll see later, will give us this output:
Coffee £2.5
Tea £1.8
Chocolate bar £1.75
Laptop £1500
Small car £12000
Doesn't look pretty, right?
Instead, we'd like the code to output a neater version with proper spacing, two digits after the decimal point, and the thousands comma separator:
Coffee £ 2.50
Tea £ 1.80
Chocolate bar £ 1.75
Laptop £ 1,500.00
Small car £ 12,000.00
We'll explore many f-string features along the way to replicate this sign.
But let's return back to the beginning.
What's An f-string?
You know about strings. So, what are f-strings? Here are some ideas:
fantastic-strings
formidable-strings
fancy-strings
fabulous-strings
friendly-strings
fascinating-strings
Right, so f-strings are all of those things. But the 'f' stands for "formatted". You can use f-strings to power up your strings with proper formatting and placeholders for Python variable names and other expressions.
There's one thing you need to know before we start: f-strings are brilliant. I like them (Can't you tell from the list above?). And many Python programmers also love them and list them as one of their favourite features in Python. So maybe the 'f' stands for favoured-strings?
Let's start with a simple example. Let's assume you want to concatenate two strings. Here's one way of doing this. I'll use the interactive REPL/Console in all examples in this article:
You use the plus operator +
to concatenate three strings: the two strings containing the first and last name and a space in between them.
There's nothing wrong with this solution. But it becomes cumbersome very quickly with more complex strings. You have to keep adding +
signs and opening and closing quotation marks.
Before we convert this to f-strings (did I already tell you that I really like f-strings?), let's have a look at another example. I deliberately left the very long line in!
Now, that's a lot more annoying to write and read. And that means you're more likely to make mistakes, too.
But there's more annoyance to be had with this option!
Argghh! You can't add an integer to a string. They're different data types. Yes, yes, there is a workaround:
But this is painful! So, let's repeat the three examples in this section, but this time we'll use f-strings:
You write an f before the quotation marks to turn the string into an f-string. You can also use a capital F, but most people use lowercase f. You can then use placeholders within the f-string by using curly brackets or braces (whatever you prefer to call them). For now, we'll place a variable name in the curly brackets. When the string is displayed, Python replaces the curly bracket placeholders with the object referenced by the variable name.
Note that if you omit the f at the beginning of the string, then you have a plain old string:
All the characters are interpreted literally as characters!
Let's look at the example with Mary, the scientist:
That's better. The code is more readable and more intuitive to write. And look at the example above, which uses age
. The value of age
is an integer. You don't need to explicitly convert the integer to a string. The f-string takes care of that for you. You'll read about how f-strings decide how to represent each object later in this article.
Before I move on, it's worth mentioning that the options shown above are not the only two options. There are others, including the old style % (and if you've never heard or seen these, try to keep it that way–they're ancient), the .format()
method, and even template strings, which can be useful in some specific situations. But I won't discuss any of these. I'll focus on f-strings in this article. I really like f-strings. (I think I said so already, not sure…)
Introducing Format Specifiers
But let's briefly return to Mary:
The data available is the person's name and the number of steps she walks in a day. The underscore in 23_391
is there to aid readability, similar to how we add commas when writing in English, such as 23,391.
Notice how you divide steps_per_day
by 24
directly in the curly brackets in the f-string. You'll get to this later.
But you want to round the result to one decimal place. You can add format specifiers to instruct the f-string on how to format the object:
You add a colon after the expression in the curly brackets to indicate you're adding a format specifier. You then add .1f
after the colon. This indicates that you require the number to be displayed as a float, hence the 'f', with one digit after the decimal point.
And you can go one step further and require zero decimal places:
The number is displayed as an integer.
Back to The Peculiar Little Shop
Here's the sign in this peculiar little shop again:
Let's work on replicating this sign using Python's f-strings. You can create a dictionary with the names and prices of the items on sale and print them out using a for
loop:
It works. But the displayed list doesn't look good. It's not clear enough for the board in the shop. Let's start working on some improvements.
Changing all numbers to include two decimal points
Let's deal with making the prices consistent regarding how many digits are displayed after the decimal point. Python defaults to the standard notation when showing numbers. Integers don't have a decimal point, as you can see in the prices for the laptop and the small car. The chocolate bar is shown as 1.75
, which is how we normally display prices. However, when dealing with prices, we wouldn't show 2.5
or 1.8
, as the prices for coffee and tea, which is what the code above shows. Instead, we should display 2.50
and 1.80
.
You can format all the numbers so they have two digits after the decimal point. I won't show the dictionary price_list
in the rest of the code snippets since this is unchanged, but I'll assume you're using the same REPL/Console session:
You add the format specifier :.2f
after price
in the curly brackets. This indicates you want the number formatted as a float with two digits after the decimal point. All prices show the pence value after the decimal point (since I'm using GBP in this example!)
Adding the thousands separator comma
But often, we would add a comma to separate thousands when writing numbers in English. You can add a comma to the f-string format specifier after the colon to deal with this:
This affects the prices displayed for the laptop and the small car as these are more than a thousand.
The format specifier is starting to look a bit weird now. There are the following elements following the colon in the curly brackets:
a comma to indicate you want the thousands separator
an
f
at the end to show you want the number displayed as a float (you can try using:,f
to see what happens)a
.2
before thef
to indicate two places after the decimal point
This is a good point to address the question you may be asking: "What are all the options I can use as format specifiers?" I won't try to make this article The Complete Guide to f-Strings, or you'll be here reading this until Christmas. I'll show a few more examples as we recreate the peculiar shop's sign, but I'm also adding an appendix highlighting a few more.
I shouldn't have written that here, as you've all scrolled down to the bottom now! I'll wait until you get back. Right, are you back?
Fixing the tabulation
The next thing we'll need to fix is the tabulation. Since the names of the products have different lengths, the prices are not aligned.
We'll look at the first curly brackets placeholder that contains item
. You can add a fill value after the colon to specify the total number of characters you wish to use to display the object:
The first pair of curly brackets now contains item:15
. This indicates that you want to use 15 characters to display this part of the string. I chose 15
since the longest item name, "chocolate bar", is 13
characters long, including the space. A width of 15
characters leaves enough space for the longest name and a couple of extra spaces for good measure!
You can determine the longest length programmatically if you wish, such as by using the following line:
And yes, you don't need to use the .keys()
method either, but I prefer to use it to improve readability when iterating over a dictionary. However, I'll stick with a hard-coded 15
in the rest of my examples.
The default alignment is 'align left'. But you can also 'align right' or 'align centre' if you prefer:
Notice the additional characters before the fill value. You can use >
to align right or ^
to align centre. The align left character <
is also valid but not required in this case. I'll stick to the default and align the product names to the left in this example since that's what's on the peculiar shop's sign.
However, when we display prices in the real world, we normally align them so that the corresponding units are aligned with each other, pence with pence and so on. Therefore, you can also add a width fill value to the second curly brackets placeholder, which contains price
:
The format specifier following price
is now :10,.2f
. The only addition from earlier is the width fill value of 10
just after the colon. This indicates that you want to use 10 characters to represent this object. I chose this value since the longest price is 12000
, but this is displayed as 12,000.00
, which contains 9
characters, including the comma and the point. I added an extra character to leave a space after the currency sign.
You could determine this value programmatically if you wish, too.
Note that, in this case, the default alignment is 'align right' since you're dealing with numbers.
And this has replicated the sign hanging behind the counter in the peculiar little shop.
There Are Other Quirky Signs in This Shop
There are other quirky signs in this shop with random facts and trivia. One of them states the speed of light. Let's replicate this sign, too:
Normally, it's not considered a good practice to use single-letter variable names. However, c is the symbol used in physics for the speed of light in vacuum, so it's fine to use a single-letter variable in this case!
There are two curly brackets placeholders in this f-string. The first one includes the comma format specifier and nothing else. The number is displayed with the thousands separators.
The second placeholder has an 'e' after the colon. You'll recall that you used an 'f' in earlier examples to show that you want the number displayed as a float. In this case, you use a different type of number. The 'e' stands for scientific notation (well, it stands for "exponent", but that's the numerical format used in science so "scientific notation" is a better term.)
You can also specify how many digits you want after the decimal point when using scientific notation:
You Can Put (Almost) Anything in An f-string
You're not restricted to just using variable names within the curly brackets in an f-string.
Let's look at an example. We'll go back to the peculiar little shop's price list sign. The shop has gained a reputation, and tourists started visiting. So, the shop's owner decided to put up a second sign showing the prices in US dollars. Here's how you can modify the code to create this new sign:
The item names are the same, so the first placeholder hasn't changed. But the rest of the f-string is different. The dollar sign replaced the pound currency sign, and the expression in the second pair of curly brackets now includes two variable names and the multiplication operator: price * gbp_usd
. You perform the calculation directly within the curly brackets in the f-string.
Let's convert the item names to upper case:
The expression in the first pair of curly brackets is now item.upper()
. Therefore, it's the value returned by the method .upper()
that's used in the string.
Let's try a more complex expression, just because we can!
The second line now shows "Buy Tea" instead of just "Tea". This is a silly sign, yes. But this example shows you can use more complex Python expressions in an f-string. The first pair of curly brackets now includes a conditional expression which returns item
if the length of item
is greater than 3
or a different string if it isn't.
There's even an f-string inside another f-string in this example. To achieve this, I use single quotation marks for the inner f-string, whereas the outer f-string uses double quotation marks. This has been the way to do this until recently. However, I'm using Python 3.12 for this article, and one of the changes implemented in Python 3.12 is that now you can use the same type of quotation marks within an f-string:
Both the inner and outer f-strings use double quotation marks in this version. But you'll need to update to Python 3.12 for this.
How Are Objects Represented In f-strings?
There's another sign in the peculiar shop which shows the date. Let's replicate it using Python's f-strings:
In fact, you've seen in the last section that you don't need to assign the value to a variable name, today
. Instead, you can call datetime.datetime.now()
directly in the f-string's curly brackets.
But I won't do this. I'll keep using the variable today
for clarity in this section.
Most customers were confused by this sign. Clearly, not everyone is used to dealing with dates in this format! The f-string will use the informal string representation of an object to display within the formatted string. This is the string representation returned by the __str__()
special method for the class. The datetime.datetime
object returns a readable time and date format similar to the official standard as its informal string representation. You can also obtain this using print()
or str()
:
The official string representation is the one returned by __repr__()
:
You can choose to use the official string representation in an f-string by using the !r
conversion after the expression:
Before we return to the customers' complaints—this version is even worse than the previous one—let's go on a small diversion.
You can use f-strings in debugging mode by adding an =
at the end. You can also add spaces around the equals sign if you wish:
In the previous examples, the curly brackets were replaced by the value of the expression within them. When you add the equals sign at the end, the curly brackets are replaced by the expression itself, followed by equals and then the value of the expression. This is useful when debugging.
But note that in this case, the official string representation that's returned by __repr__()
is used since this is more useful when debugging. You can use the !s
conversion if you want to use the informal representation returned by __str__()
:
The shop owner finally gets some help with his Python coding, and he's ready to deploy the following version:
The format specifiers you use now are specific to the datetime.datetime
object. If you're familiar with the datetime
module, and with the worst-named functions in Python, strftime()
and strptime()
, then you may have used these datetime
format codes. You can find the datetime
format codes here.
And on the topic of further reading:
To read more about dates and times, you can read Chapter 9 | Dealing With Dates and Times in Python in The Python Coding Book and 5:30am • Timezone Headaches if you want to deal with timezones.
And if you want to read more about the informal and official string representations using
__str__()
and__repr__()
, you can read the Real Python article I wrote on this topic.
Final Word
Did I already tell you I like f-strings? And I'm not alone. They provide a way of concatenating strings and formatting them that's readable and flexible. There are some instances when you can't use f-strings, but I won't talk about these here. In most cases, if you need a string that's not just a simple fixed string, you need an f-string.
There's an appendix below if you're interested in reading a bit more.
Code in this article uses Python 3.12
Appendix: And Now For Some More Format Specifiers
Let's look at a few more format specifiers.
Thousands separators and presentation types
You've already used the comma format specifier to include commas as a thousands separator. You can also use an underscore instead.
And you've used 'f' and 'e' to represent floats or scientific notation. Here are a few more types:
Positive and negative numbers, and percentages
Let's play with some random numbers between -1 and 1 now:
The outputs look untidy since some numbers have a minus sign and others don't. Let's add a plus sign in front of positive numbers by adding +
after the colon in the f-string's curly brackets:
But we rarely want to show the plus sign in front of positive numbers. So, let's replace the plus sign with a space for positive numbers only:
Did you note the extra space after the colon? The format specifier is a space!
Let's replicate this using a precision of 4:
Notice the space after the colon in the last example.
Finally, you can replace the type specifier from 'f' to '%' to show the number as a percentage:
The numbers are shown as a percentage now.
One more example
Let's carry on with the set of numbers created in the previous section and explore another type option by replacing 'f' with 'n'. Here are both versions, the first one using 'f' and the second using 'n':
Did you look closely? Did you spot the difference?
No? Neither did I. But some of you may have seen a different output when you tried this on your computer.
Eh?!
Let me demonstrate by virtually moving to another country. I'll pick Germany for this example:
When using type 'f', the output is the same as before. However, since the locale is now set to Germany, the format specifier 'n' replaces the decimal point with a comma, which is consistent with how numbers are displayed in this locale setting.
You can read more about format specifiers in the docs.
Stop Stack
#37
The Python Coding Place has moved on to the next stage, with a new website (almost) and the next new set of resources that I started to roll out: video courses. I started releasing the first course this week. It's called A Python Tale and it's designed for beginners. But I have many other courses at advanced stages of planning and preparation, so you can expect many new courses at The Place over the coming months for beginners and intermediate learners. If you know of any beginners starting to learn to code, you can recommend A Python Tale. I'll say more about membership of The Python Coding Place in future Stop Stacks.
Recently published articles on The Python Coding Stack:
Python City (Monty and The White Room Series #3) Part 3 in the Monty and The White Room Series
This Page Is Intentionally Left Blank • The Story of
None
(Paid article) Understanding Python'sNone
The Function Room (Monty and The White Room Series #2) Part 2 in the Monty and The White Room Series • Understanding functions
Monty and The White Room (Monty and The White Room Series #1) Understanding a Python program through The White Room analogy • Part 1
5:30am • Timezone Headaches (Part 1) I need Python's help to figure out the time of my talk • Dealing with timezones and daylight saving with Python's
zoneinfo
anddatetime
modules • The first article in a two-part mini-series
Recently published articles on Breaking the Rules, my other substack about narrative technical writing:
The Selfish Reason (Ep. 13) Another reason for authors to innovate • Enjoying the writing process
The Consequential Detail (Ep. 12). Can a single letter or one blank line make a difference? (Spoiler Alert: Yes)
The Unexpected Audience (Ep. 11). What I'm learning from listening to Feynman's physics lectures
The Story So Far (Mid-Season* Review). Are you back from your holidays? Catch up with what you've missed
Broken Rules (Ep. 10). Let's not lose sight of why it's good to break the rules—sometimes
Frame It • Part 2 (Ep. 9). Why and when to use story-framing
Stats on the Stack
Age: 6 months, 3 weeks, and 4 days old
Number of articles: 37
Subscribers: 1,266
Each article is the result of years of experience and many hours of work. Hope you enjoy each one and find them useful. If you're in a position to do so, you can support this Substack further with a paid subscription. In addition to supporting this work, you'll get access to the full archive of articles and some paid-only articles.
Your code does not work properly with the proportional italic font that's shown on the chalkboard!
How can I make it work also for non monospaced fonts?