Butter Berries, An Elusive Delicacy
How my quest to find butter berries at the supermarket led to musings about Python lists and dictionaries and more
My daughter has a swimming lesson every Saturday morning. My wife takes her. My son and I meet them directly at the supermarket after the lesson to get our weekly shopping done.
Last Saturday, my son and I were a bit early so we started going round the supermarket filling the trolley. I texted my wife to see what she needed:
Get butter berries and flour. Will be there soon.
Right, let's start with the fruit aisle.
I'm not as knowledgeable about fruit as I should be. I know the common types of berries—blueberries, blackberries, raspberries, strawberries—but I'm not quite sure I know the other fancier varieties. Did our local supermarket even stock butter berries?
If they did, they weren't in the obvious place next to the strawberries. "We should ask a staff member", my son suggested sensibly. No way! We'll find them. All by ourselves.
Ten long minutes later, my wife and daughter arrived, finally.
"Can't find the butter berries anywhere"
"The what?"
"The butter berries. What are they, anyway?"
My wife's quizzical look prompted me to flick my phone out of my pocket, and a few carefully crafted swipes later, I'm showing her the text message she had sent me earlier.
"So?" she asked. "I need butter and flour to make a cake, and any type of berry will do."
Oh!
Get butter berries and flour
The comma. The comma's missing:
Get butter, berries and flour
Well, if it were me, it'd be two commas missing since I'd use an Oxford comma, but that's another matter.
In shopping lists, just like Python lists, commas matter.
Weekly Shop, Python-Style
Let's re-imagine this trip to the supermarket through Python spectacles. In particular, I'll look at some of the fundamental data types and how we can map this real-life situation to Python data structures.
I'll start with the shopping list, and then move on to the supermarket aisles.
Here's the shopping list first. You already know the issue with the missing comma. I'll add it in later. But for now, spot the missing comma:
You use the unpacking operator *
with shopping_list
in print()
to print out the individual items within the list rather than the list as a whole. And here's the output from this code:
Time to start your shopping:
Biscuits
Bread
ButterBerries
Bananas
Bagels
I'll imagine a couple of different scenarios next. In the first one, you're shopping in an unfamiliar supermarket. You don't know where anything is. Later, you'll do the same shop in your local supermarket, which you know well. We'll explore Python's data structures while shopping.
Rambling Through The Aisles
You take your shopping list to a supermarket. But you haven't been to this supermarket before. Its layout is unfamiliar. You start going through your shopping list:
Time to start your shopping:
Biscuits
Bread
ButterBerries
Bananas
Bagels
Biscuits is first on the list. You start walking up the first aisle and down the second one. Then back up the third aisle, and so on. You scan every shelf until you find the biscuits. Great.
Now, start again. This time, you're looking for bread. You find it, eventually.
Butter berries are next on the list. I know you know how this story ends. But let's play along. You walk up and down every aisle in the supermarket. Then back again. And again. You scan every shelf, but there are no signs of butter berries.
You would have saved plenty of time if you hadn't missed the comma when you created the list. But that's not the whole problem. In this scenario, you have a list
of the items you want to buy. But this list has no information on whether the items are available and where they're located.
Let's combine this list
with another built-in Python data structure, the dictionary. We'll also use some generator expressions and named tuples for good measure.
An Efficient Trip to Your Local Supermarket
You didn't like the experience in the unfamiliar supermarket. You like to shop in your local one, which you know well. You know your local supermarket so well you created a Python dictionary to keep track of the aisle and section of each item that the supermarket stocks. The aisles are numbered, and letters are used for the sections.
Here's the first version of this dictionary:
The dictionary's keys are strings with the items' names. The value associated with each key is a tuple containing the aisle number and the section.
This works. However, fetching data from this dictionary can be a bit messy. Let's assume you want the aisle number where you can find bananas:
supermarket["Bananas"][0]
or
supermarket.get("Bananas")[0]
Both versions return 5
, the aisle number where you'll find the bananas.
There's nothing wrong with this dictionary with tuple values. However, especially for more complex use cases, the need to index using an integer can lead to code that's less readable and more susceptible to bugs. It's not immediately evident that the index [0]
refers to the aisle number.
I've been using named tuples instead of tuples recently in similar scenarios. You'll see why this makes the code more readable soon.
I won't deal with the details about named tuples in this article. If you want to read more about this variant of the tuple data type, you can read Sunrise: A Python Turtle Animation Using Named Tuples, an article on my pre-Substack blog.
There's more than one way to create named tuples in Python. I'll use the option that relies on the typing
module. Yes, the typing
module–the one that deals with type hints. Let's explore this in a Console/REPL session first:
You import NamedTuple
from typing
and use it as a base to create the subclass SupermarketShelves
. You also define the named tuple's fields within the body of the class definition. Since you're using the named tuple version in the typing
module, you must also add type annotations to show that the field aisle
is an integer and section
is a string.
You create an instance of SupermarketShelves
, which is a named tuple. As you can see in the last expression, shelves
is also a tuple. As you'd expect, a named tuple is a tuple!
You can also use named arguments to create the named tuple for extra readability:
You could use shelves
in the same way as a standard tuple:
But the point of using named tuples is to use the field names instead of indices:
The latter version improves readability, and this matters in longer programs.
Now, you can update the supermarket
variable in the main script:
You replaced the standard tuples in the previous version with named tuples. Now, you can add the shopping list you had earlier back in the code:
Preparing your supermarket battle plan
You're now equipped with a shopping list, which is a Python list, and a map of the supermarket, which is a Python dictionary with named tuples as values.
But you want to be prepared before tackling the supermarket trip. So, you decide to order your shopping list based on the aisle numbers. This strategy will help you make your way through the supermarket efficiently.
Let's create a function to sort the shopping list in the optimal order. Here's the first attempt:
The function order_shopping_list()
has two parameters: shopping_list
and supermarket
. The function sorts the shopping list using sorted()
. You assign a lambda
function to the key
parameter. This is the parameter that's used to determine the order for sorting the items in shopping_list
. The lambda
function returns the aisle value, and this is the number used to determine the shopping list order.
You may be thinking: "This doesn't need to be in a function at all". And you'd be right, for now. But you'll see that you'll hit a problem soon, and we'll need to make this function a bit more complex—but not too much!
Here's the output from this code:
Traceback (most recent call last):
File ... line 32, in <module>
items_to_buy = order_shopping_list(shopping_list, supermarket)
File ... line 25, in order_shopping_list
sorted_items = sorted(
File ... line 27, in <lambda>
key=lambda item: supermarket[item].aisle,
KeyError: 'ButterBerries'
Not a good start! You can see the problem in the last line of the error message. When trying to access supermarket[item]
, a KeyError
is raised when the program looks for "ButterBerries"
. There is no such key in the dictionary supermarket
.
That's the comma we forgot to add in shopping_list
after the item "Butter"
. Even though the strings "Butter"
and "Berries"
are on different lines, Python concatenates these into a single string no comma separates them.
Let's add the comma:
But you still get a similar KeyError
when you run this code. This time, the offending item is "Bagels"
:
...
KeyError: 'Bagels'
There's nothing wrong with the commas in this case! But the supermarket doesn't stock bagels. You can see it's not present in the dictionary supermarket
.
So you get a KeyError
when you try to get the value of an item that's not in the supermarket.
You can use the dictionary's .get()
method instead of using square brackets to avoid this KeyError
:
But now you get a different error:
Traceback (most recent call last):
File ... line 32, in <module>
items_to_buy = order_shopping_list(shopping_list, supermarket)
File ... line 25, in order_shopping_list
sorted_items = sorted(
File ... line 27, in <lambda>
key=lambda item: supermarket.get(item).aisle,
AttributeError: 'NoneType' object has no attribute 'aisle'
The problem is still with the return value of the lambda
function. Let's go back to the Console/REPL to explore this:
When you use the .get()
dictionary method instead of square brackets [ ]
and you try to fetch a key that doesn't exist, the method returns a default value rather than raise a KeyError
. The .get()
method returns None
in this case, unless you specify a different default value.
But the object None
doesn't have an attribute aisle
, as the final error message states. So, you get an AttributeError
.
Sorting the shopping list into available and unavailable items
Let's sort the items into two separate variables. One of these will contain the items present in the supermarket. These will be ordered based on their aisle numbers. The second variable will contain the items that aren't in stock:
You used two generator expressions to separate the items in stock and those that are not available. You only need to use sorted()
on available_items
since the items that aren't available won't make you more or less efficient in your shopping!
Note that order_shopping_list()
returns two values. One of these is a list, and the other is a generator expression. The built-in sorted()
returns a list. If you prefer both of these to be lists, you can cast unavailable_items
to a list. However, this is not necessary in most cases since both are iterables.
Here's the output from this code:
Time to start your shopping:
Butter
Bread
Biscuits
Berries
Bananas
I'm afraid these items are not stocked:
Bagels
I'll finish this article by showing the whole code, but I'm reverting to the original error where the comma after "Butter"
was missing. After all, this article is about "butter berries"!
And as the output shows, there are no butter berries, I'm afraid:
Time to start your shopping:
Bread
Biscuits
Bananas
I'm afraid these items are not stocked:
ButterBerries
Bagels
Final Words
This trip to the supermarket, which did really happen, I promise, led us to explore several Python topics, from the missing comma, which leads to strings being concatenated, to using the properties of lists and dictionaries effectively. We used named tuples to replace normal tuples or other core data types. We even used a couple of generator expressions for good measure.
And in case you're wondering, the butter, berries, and flour were used to bake a cake, but it was for my wife to take to her sister's house, so I never got to try it!
Code in this article uses Python 3.11
Stop Stack
#28
I’m thinking of updating how I separate content for free and paid subscribers. I want to make sure most of my articles remain free. But I also want to reward the growing number of paid subscribers. If you have models you’ve seen elsewhere on Substack you like, please do let me know. I have some ideas already, but I’m still considering all options.
Recently published articles on The Python Coding Stack:
Pay As You Go • Generate Data Using Generators (Data Structure Categories #7) Generators • Part 7 of the Data Structure Categories Series
Clearing The Deque—Tidying My Daughter's Soft Toys • A Python Picture Story Exploring Python's
deque
data structure through a picture story. [It's pronounced "deck"]The Final Year at Hogwarts School of Codecraft and Algorithmancy (Harry Potter OOP Series #7) Year 7 at Hogwarts School of Codecraft and Algorithmancy • Class methods and static methods
Tap, Tap, Tap on The Tiny
turtle
Typewriter. A mini-post usingfunctools.partial()
andlambda
functions to hack keybindings in Python'sturtle
modulePython Quirks? Party Tricks? Peculiarities Revealed… (Paid article) Three "weird" Python behaviours that aren't weird at all
Recently published articles on Breaking the Rules, my other substack about narrative technical writing:
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
The Rhythm of Your Words (Ep. 8). Can you control your audience's pace and rhythm when they read your article?
A Near-Perfect Picture (Ep. 7). Sampling theory for technical article-writing • Conceptual resolution
The Wrong Picture (Ep. 6). How I messed up • When an analogy doesn't work
Stats on the Stack
Age: 4 months, 3 weeks, and 6 days old
Number of articles: 28
Subscribers: 979