Python · Chapter 2 · Concept lesson · 6 min read

Finding values by position, by key, and one by one

A list finds values by position, a dictionary finds them by key, and a loop visits them one at a time while an accumulator remembers what it has seen so far.

The idea

A single price fits in a variable. A cart, a week of orders or a stock sheet needs a collection: one name that holds many values. Python has two you will use all the time, and the difference is how you find a value inside.

A list finds values by position: first, second, last. A dictionary finds them by key: "web", "marketplace", a product name. Pick the one that matches the question you ask. "What happened on day 3?" is a position. "How many orders came from the app?" is a key.

Figure 1

Orders per day, first week of January: positions and slices

daily = [1, 8, 6, 4, 10, 9, 5]
positiondateorders
02023-01-011
12023-01-028
22023-01-036
32023-01-044
42023-01-0510
52023-01-069
62023-01-075
daily[1:4]
positionorders
18
26
34

Step 1 of 4 A list keeps its values in order, and every value has a position. Positions start at 0, so the first day is daily[0], which is 1.

  1. A list keeps its values in order, and every value has a position. Positions start at 0, so the first day is daily[0], which is 1.
  2. A negative position counts from the end. daily[-1] is the last value, 5, without having to know the length.
  3. A slice daily[1:4] starts at position 1 and stops before position 4. Three values come out: positions 1, 2 and 3.
  4. sum(daily[1:4]) is 18 orders for Monday to Wednesday. The end of a slice is exclusive, so daily[1:4] has 4 - 1 = 3 elements.

Why it exists

Imagine totalling a cart with one variable per line: line1, line2, line3. It works for three lines and falls apart at four. And you cannot write code that works for a cart of any size, because the names are fixed when you type the program. A list lets the data decide how big it is. A dictionary solves the matching problem: when stock is "Alder Oak Coffee Table: 12", scanning a list for the name each time is slow and clumsy, while a dictionary jumps straight to the answer.

Together with a loop, these are the first version of what pandas does for you later. A pandas column acts like a list, a row like a dictionary, and groupby is the counting loop below, written once and run on millions of rows.

How it works under the hood

Lists: position, slices and length

A list is ordered, and each item has a position starting at 0. len(daily) is 7, the last position is len(daily) - 1, and daily[-1] reaches it from the other end. A slice daily[start:stop] takes from start up to but not including stop, so the number of items is stop - start. Slices make copies, and slicing past the end is forgiving: daily[5:100] just returns what exists. A single position past the end is an IndexError.

Lists can be changed in place. append adds one item, and daily[0] = x replaces one. Two names pointing at the same list see the same change, which surprises people in chapter 3.

Dictionaries: keys and values

A dictionary is a set of key-value pairs. Keys are unique, and finding a value by key takes the same short time whether there are 3 keys or 3 million. That is how pandas finds a column by name, and how SQL finds a row by its primary key. d['web'] reads a key and fails with KeyError if it is not there. d.get('web', 0) reads it and falls back to 0 when it is not. Assigning d['app'] = 1 adds the key if it is new and replaces the value if it exists.

Loops and the accumulator

A for loop runs its indented block once per item, with the loop variable set to the current one. Most real loops do the same small job: they keep something that grows. That something is the accumulator, and it must exist before the loop starts.

Figure 2

Counting orders by channel with a dictionary

channels (orders 1001 to 1010)
order_idchannel
1001web
1002web
1003marketplace
1004marketplace
1005web
1006marketplace
1007marketplace
1008web
1009app
1010web
counts (a dictionary)
keyvalue
web5
marketplace4
app1

Step 1 of 4 Ten orders, each with a channel. We want one number per channel, and the channels are not known in advance.

  1. Ten orders, each with a channel. We want one number per channel, and the channels are not known in advance.
  2. A dictionary holds pairs: a key and its value. Start empty. The first order is web, which is not a key yet, so counts.get('web', 0) gives 0 and we store 0 + 1.
  3. Every later web finds its key and adds 1. Marketplace and app appear for the first time when we meet them, and each starts at 1.
  4. After the loop, the dictionary is the answer: 5 web, 4 marketplace, 1 app, ten orders in total. Looking up counts['app'] is instant, no matter how many keys there are.

Counting by channel is an accumulator whose value is a dictionary. A total is an accumulator whose value is a number.

Figure 3

A running total over a cart of four lines

Order 1004: the cart
priceqty
92.991
20.992
149.001
66.992
total after each pass
passtotal
00.00
192.99
2134.97
3283.97
4417.95

Step 1 of 4 Before the loop, total = 0. This is the accumulator: a variable that lives outside the loop and remembers what has been seen so far.

  1. Before the loop, total = 0. This is the accumulator: a variable that lives outside the loop and remembers what has been seen so far.
  2. Pass 1 takes the first line: total += price * qty adds 92.99 × 1. The loop body ran once, and total is now 92.99.
  3. Passes 2 and 3 add 41.98 and 149.00. Each pass starts from the total the previous one left behind.
  4. Pass 4 adds 133.98 and the loop ends because there are no more lines. The cart is worth 417.95. Put total = 0 inside the loop and every pass would forget the last.

The shape is always the same: create the accumulator, loop, update it from the current item, use it after the loop. If the answer is wrong, the bug is almost always one of these four: it was created inside the loop, it was updated with = instead of +=, the update is indented outside the loop, or the result is used before the loop is done.

Comprehensions

A list comprehension is a loop that builds a list, written in one line: [p for p in products if p['price'] < 20]. Read it as "p, for each p in products, if the price is under 20". It is the same loop, with the accumulator hidden. Use it when you are filtering or transforming; use a normal loop when the body needs more than one step.

Which one to reach for

Ask what you will do with the data. If the order matters and you will walk through it, slice it or take the first and last, use a list: orders by day, prices in a cart. If you will look things up by a name, or count by a label, use a dictionary: stock by product, orders by channel. When you need both, hold a list of dictionaries, one per row, with the same keys in each. That is exactly the shape of a table, and the shape pandas builds a DataFrame from.

Common misconceptions

"The first item of a list is number 1"

Positions start at 0, so daily[1] is the second day. It follows from the way a position works: it is the distance from the start, and the first item is 0 steps away. When a result is off by one, check whether you counted from 1 and Python from 0. For a slice, remember that the end is excluded: daily[0:7] is all seven days.

"A dictionary has an order like a list does"

It remembers the order keys were added in, but you can't ask for "the third entry" the way you ask a list. You ask for a key. If you need "the top channel", you loop through the pairs and keep the largest one yourself, or sort the items, which is what the levels do next.

"I can change the list I am looping over"

Adding or removing items from a list while looping over it makes the loop skip items or run forever. Build a new list instead, either with a comprehension or by appending to an empty list. The loop reads one list and writes another, and neither disturbs the other.

Check yourself

One question. Answering it ticks this lesson off on the map; it never locks a level.

Counting orders by channel, a learner writes counts[channel] += 1 inside the loop, without .get and without checking if the key exists. The first order is web. What happens?

Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.