Chapter 10: Tuples and Dictionaries

Every programming language has to answer one very plain question: when a program mentions a name, how does the language find the thing that name stands for? A list would be the obvious answer — keep the names in order and search through them. But searching means looking at one name, then the next, then the next, and a large program has thousands of names.

Python, first released by Guido van Rossum in 1991, answered differently. It kept names and the values they stand for as pairs, in a structure that is looked up by the name itself rather than by position. That structure is the dictionary, and Python leans on it so heavily that the language uses it to build itself: a module keeps its names in a dictionary, and so does a class, and so does almost every object you make.

There was a second problem to solve as well. A list is easy to change, and that is usually what you want — but not always. The three coordinates of a point, the day-month-year of a date, a record lifted out of a register: these are collections that should stay exactly as they were made. For these, Python provided a sequence that closes once it is built. It is called the tuple.

This chapter is about both of them — the sequence that cannot be changed, and the collection that is found by name.

Fig. 10.0

Fig. 10.0 – Tuple and dictionary storage mechanisms.

NoteCuriosity Corner

Q1. Python already has the list. Why would anyone want a second sequence type that refuses to be changed? Q2. A tuple cannot be altered once it is made. How, then, can the data held inside one still end up different from what you put in? Q3. Why does writing a single value inside round brackets not give you a tuple at all — and which one character repairs it? Q4. A dictionary is not searched by position. So when you ask it for one value out of a thousand, how does it know where to look? Q5. What happens if you ask a dictionary for a key it does not have, and is there a way to ask that does not stop the program?

Keep these five questions in mind as you read. Each one is answered somewhere in this chapter, at the point where the idea behind it is explained.

10.1 Introduction to Tuples

Suppose you are noting down one bus stop from a timetable: its name, and the platform it leaves from. Those two facts belong together, they have a definite order, and neither of them is going to change while your program runs. What you want is a small container that holds several values in a fixed order and then stays as it is.

Python gives you exactly that container. A tuple is an ordered sequence of values, written inside round brackets with the values separated by commas. Each value stored in it is called an element, and the elements need not be of the same type — a tuple can hold an integer, a floating-point number, a string, a list, or even another tuple, all at once.

Because a tuple is ordered, every element sits at a fixed numbered position. That position number is called the index, and, as with a string or a list, counting starts at 0 and not at 1. So the first element of a tuple is at index 0, the second at index 1, and so on.

NoteDefinition

(A tuple is an ordered sequence of elements, written within round brackets and separated by commas, whose elements are accessed by index starting from 0 and cannot be changed once the tuple has been created.)

You have already met two ordered sequences in Python — the string and the list. The tuple is the third, and it borrows almost all its behaviour from them. What sets it apart is the last clause of that definition, and the rest of this section builds up to it.

10.1.1 Creating a Tuple

A tuple is created by writing its elements inside round brackets, separated by commas, and assigning the whole thing to a variable.

>>> marks = (72, 65, 88, 91, 54)
>>> marks
(72, 65, 88, 91, 54)

All five elements here are integers, but nothing forces that. The next tuple records one book in a school library register — its title, the year it was bought, its price and whether it is currently on the shelf.

>>> book = ('Ganit Prakash', 2019, 245.75, True)
>>> book
('Ganit Prakash', 2019, 245.75, True)

An element may itself be a whole collection. Below, the third element is a list, and in the tuple after it the second element is another tuple.

>>> reading = (101, 'Mon', [12, 15])
>>> reading
(101, 'Mon', [12, 15])
>>> stop = ('Sanganer', (26, 75))
>>> stop
('Sanganer', (26, 75))

creating a tuple

tuple_name = (element1, element2, element3)

Parts: tuple_name is any valid Python variable name; element1, element2, element3 are the values to be stored, in the order you want them kept; the round brackets enclose the whole sequence and the commas separate the elements. Returns / Effect: a tuple object is built and the name on the left is made to refer to it. Example:

>>> stop = ('Sanganer', 4, 6.30)
>>> stop
('Sanganer', 4, 6.30)

Two edge cases in this syntax deserve care, because both of them trip students up.

The empty tuple and the one-element tuple

An empty tuple is written as a pair of brackets with nothing inside, and Python prints it back the same way.

>>> empty = ()
>>> empty
()
>>> type(empty)
<class 'tuple'>

A tuple of one element, however, is not written the way you would expect. Round brackets in Python already have an older job — they group an expression, as in (3 + 4) * 2. So when Python sees a single value inside brackets, it reads the brackets as grouping and hands back the value itself, unchanged.

>>> fare = (45)
>>> fare
45
>>> type(fare)
<class 'int'>

The name fare is now an ordinary integer. What actually makes a tuple is not the brackets at all — it is the comma. Put a comma after the single element and Python has no choice but to read the whole thing as a sequence.

>>> fare = (45,)
>>> fare
(45,)
>>> type(fare)
<class 'tuple'>

Notice how Python prints it: (45,), with the comma kept in. That trailing comma in the output is Python’s way of telling you, unmistakably, that this is a one-element tuple and not a plain number.

Q3. Why does writing a single value inside round brackets not give you a tuple at all — and which one character repairs it?

A. Round brackets are already used in Python to group an expression, so (45) is read as the number 45 with brackets around it, not as a sequence. The character that repairs it is the comma: (45,) cannot be read as a grouped expression, so Python builds a tuple of one element.

forgetting the comma in a one-element tuple What students write:

>>> fare = (45)
>>> len(fare)
TypeError: object of type 'int' has no len()

What happens: fare was never a tuple. It is an integer, and an integer has no length, so the error surfaces only later — at the line that treats fare as a sequence, not at the line that created it. The fix: write fare = (45,). The comma, not the bracket, is what makes a tuple.

A tuple without brackets

Since the comma is what does the work, the brackets can often be dropped altogether. A plain list of comma-separated values is treated as a tuple by default.

>>> point = 26, 75
>>> point
(26, 75)
>>> type(point)
<class 'tuple'>

Python has read 26, 75 as a tuple and, when asked to display it, printed it back with the brackets restored. Writing a tuple this way is common and perfectly correct, but in a program that others will read, the brackets are worth keeping — they make the intention visible at a glance.

Q. If the comma is what builds a tuple, what does total = 45, create?

A. A tuple of one element, (45,) — not the integer 45. A stray comma at the end of a line is easy to type by accident, and this is the silent bug it produces.

10.1.2 Accessing Elements in a Tuple

Once a tuple exists, you reach a single element by writing its index in square brackets after the tuple’s name — the same notation you already use for strings and lists. The tuple below records the maximum temperature recorded on six consecutive days.

>>> temp = (31, 34, 29, 27, 33, 30)
>>> temp[0]
31
>>> temp[4]
33

The first element is at index 0, so temp[4] is the fifth reading, 33, and not the fourth. This off-by-one relationship between position and index is worth fixing firmly in mind, because almost every indexing mistake in Python comes from forgetting it. Fig. 10.1 shows the two index scales that run along this tuple.

Fig. 10.1

Fig. 10.1 – Positive and negative indices of a tuple.

The second scale in the figure is the negative one. An index of -1 means the last element, -2 the second last, and so on backwards to -6 for the first. This saves you from having to know the length of a tuple in order to reach its final element.

>>> temp[-1]
30
>>> temp[-6]
31

The index need not be typed as a plain number. Anything that evaluates to an integer will serve, so an expression works just as well — Python evaluates it first and then uses the result as the index.

>>> temp[2 + 3]
30

If the index falls outside the range the tuple actually has, Python stops with an error rather than guessing. A tuple of six elements has valid indices 0 to 5 going forward, and -1 to -6 going backward; anything else is out of range.

>>> temp[6]
IndexError: tuple index out of range

Q. The valid negative indices here stop at -6, and the valid positive ones start at 0. So what does temp[-0] return?

A. 31, the first element. In arithmetic -0 is simply 0, so Python sees the index 0. There is no negative index that reaches the first element from the right-hand side.

Create the tuple temp shown above and, without running anything yet, write down on paper what each of temp[3], temp[-3] and temp[1 + 1] will give. Then check all three at the shell prompt.

10.1.3 Tuple is Immutable

Everything so far has been shared with the list. Here the two part company. A tuple is immutable — once it has been built, its elements cannot be replaced, removed or added to. Any attempt to assign a new value to one of its positions is refused.

>>> temp = (31, 34, 29, 27, 33, 30)
>>> temp[2] = 35
TypeError: 'tuple' object does not support item assignment

Read the message carefully: it does not say the value 35 is wrong, or the index 2 is wrong. It says the tuple does not support item assignment at all. A list would have accepted that same line without complaint, because a list is mutable — its contents can be changed in place after it is created.

NoteDefinition

(An immutable object is one whose value cannot be changed after it has been created; any operation that appears to change it in fact builds a new object.)

That last clause explains what happens when you seem to “change” a tuple anyway:

>>> temp = (31, 34, 35, 27, 33, 30)
>>> temp
(31, 34, 35, 27, 33, 30)

Nothing was modified here. A brand-new tuple was built from the six values written on the right, and the name temp was then made to refer to that new tuple instead of the old one. The original tuple was not edited; it was replaced, and the name was pointed elsewhere.

Table 10.1 – List vs tuple

Feature List Tuple
How it is written Elements inside square brackets, e.g. [31, 34, 29] Elements inside round brackets, e.g. (31, 34, 29)
Changing an element Allowed; data[2] = 35 replaces the element in place Refused; raises TypeError
Adding or removing elements Allowed after creation Not possible; a new tuple must be built
Effect of assigning to the name The same object may be modified A different object is created and the name is repointed
Suits data that is expected to change as the program runs must stay exactly as it was recorded

So why keep a type that refuses to be edited? Because refusal is the point. When a set of values genuinely must not change — a fixed set of coordinates, the columns of one record read out of a register, a table of month names — storing it in a tuple means no later line of the program can quietly damage it. An accidental assignment becomes an error you see immediately, instead of a wrong value you discover much later. Immutability also lets Python handle a tuple more simply than a list, which is why stepping through a tuple is generally quicker than stepping through a list of the same size.

Q1. Python already has the list. Why would anyone want a second sequence type that refuses to be changed?

A. Because some data should not be changed, and a type that refuses the change turns a silent bug into an immediate error. Data stored in a tuple cannot be edited by any later part of the program, so it is protected against accidental modification — and, being fixed, it can also be handled more efficiently than a list of the same size.

Immutable, but not always frozen

There is one situation that looks like a contradiction until you see what is really being protected. A tuple may hold a mutable element — a list, for instance. The tuple will not let you replace that element, but nothing stops you from changing what is inside it.

>>> reading = (101, 'Mon', [12, 15])
>>> reading[2][0] = 18
>>> reading
(101, 'Mon', [18, 15])

The tuple still has exactly three elements, and its third element is still the same list object it always was. What changed is the contents of that list, and the list itself is perfectly willing to be changed. Try instead to swap the list for a different one, and the tuple refuses as firmly as before:

>>> reading[2] = [18, 15]
TypeError: 'tuple' object does not support item assignment

The rule, then, is precise: a tuple fixes which objects it holds, not what those objects contain. Fig. 10.2 makes the distinction visible.

Fig. 10.2

Fig. 10.2 – A tuple holding a mutable list.

Q2. A tuple cannot be altered once it is made. How, then, can the data held inside one still end up different from what you put in?

A. A tuple fixes which objects it holds, not what those objects contain. If one of its elements is itself a mutable object such as a list, that list can still be changed from inside — the tuple continues to hold the very same list, so the change shows up when the tuple is displayed.

Build the tuple stop = ('Sanganer', (26, 75)), whose second element is itself a tuple. Predict what happens when you attempt stop[1][0] = 27, then run it. Explain, in one line, why the result differs from the reading example above.

10.2 Tuple Operations

A tuple cannot be edited, but that does not leave you helpless with one. Python provides a small set of operations that build new tuples out of existing ones, test what a tuple holds, and pull out a portion of it. There are four such operations, and each of them is written with a symbol you have already used on strings and lists — which is the point. The same operator behaves in the same spirit on every ordered sequence in Python.

Keep the rule of Section 10.1.3 in view while reading this section. None of these operations alters the tuple you start with. Where an operation appears to grow or shorten a tuple, what has really happened is that a fresh tuple was built and handed back to you.

10.2.1 Concatenation

Two tuples are joined end to end with the + operator. This is called concatenation, and the result is a new tuple containing the elements of the first followed by the elements of the second, in that order.

>>> morning = (6, 7, 8)
>>> evening = (17, 18, 19)
>>> morning + evening
(6, 7, 8, 17, 18, 19)

The order matters, because a tuple is an ordered sequence. Writing evening + morning would give (17, 18, 19, 6, 7, 8) — the same six values, but a different tuple.

Neither morning nor evening has been touched by that line. The joined result was simply displayed and then discarded, because nothing was done with it. To keep it, give it a name:

>>> departures = morning + evening
>>> departures
(6, 7, 8, 17, 18, 19)
>>> morning
(6, 7, 8)
Extending a tuple

Since a tuple cannot grow in place, concatenation is also how you add an element to one. The trick is to write the new element as a tuple of its own — which, from Section 10.1.1, means remembering the comma.

>>> routes = ('A1', 'A2', 'A3')
>>> routes = routes + ('A4',)
>>> routes
('A1', 'A2', 'A3', 'A4')

More than one element can be added in the same way, and there the comma comes naturally:

>>> routes = routes + ('A5', 'A6')
>>> routes
('A1', 'A2', 'A3', 'A4', 'A5', 'A6')

Nothing here contradicts immutability. The original three-element tuple was never modified. A four-element tuple was built from it, the name routes was made to refer to that new tuple, and the old one — now unreachable — was left behind. This is exactly the pattern you saw when a tuple appeared to be “changed” in Section 10.1.3.

joining a tuple to something that is not a tuple What students write:

>>> routes = ('A1', 'A2')
>>> routes = routes + 'A3'

What happens:

TypeError: can only concatenate tuple (not "str") to tuple

The fix: write routes = routes + ('A3',). Both operands of + must be tuples. A string, a list or a bare value on the right will be refused, however sequence-like it may look.

10.2.2 Repetition

The * operator repeats a tuple a stated number of times. This is called repetition, and it needs a tuple on one side and an integer on the other.

>>> pair = (0, 1)
>>> pair * 3
(0, 1, 0, 1, 0, 1)

Read the result carefully. The whole tuple is repeated three times, one full copy after another — you do not get each element repeated three times in place. Repetition is most useful for setting up a sequence of identical starting values, such as a row of empty seats:

>>> slot = ('Free',)
>>> slot * 4
('Free', 'Free', 'Free', 'Free')

The second operand must be an integer. A float, however round it looks, is refused, because there is no sensible meaning for repeating a sequence two-and-a-half times.

>>> pair * 2.0
TypeError: can't multiply sequence by non-int of type 'float'

Q. What does pair * 0 give, and what about pair * -2?

A. Both give the empty tuple (). Zero copies of a sequence is nothing at all, and Python treats any negative count the same way rather than raising an error — which is worth knowing, because a repetition count that has gone negative through a calculation will fail silently rather than loudly.

10.2.3 Membership

To find out whether a value is present in a tuple, use the in operator. It is called the membership operator, and it gives back True if the value is one of the tuple’s elements and False otherwise.

>>> subjects = ('Physics', 'Chemistry', 'Maths', 'Biology')
>>> 'Maths' in subjects
True
>>> 'Hindi' in subjects
False

Its partner, not in, reverses the answer. It is True exactly when the value is absent.

>>> 'Hindi' not in subjects
True
>>> 'Maths' not in subjects
False

This is a genuine convenience. Without it you would have to walk through the tuple index by index, comparing each element yourself, and stop when you found a match. The in operator does that search for you and reports only the verdict.

What it compares are whole elements, never parts of them. A tuple of strings is not searched for a fragment of a string:

>>> 'Math' in subjects
False

The same holds when the elements are themselves tuples. Below, the readings tuple has two elements, each a tuple of two values, so a match must be one of those complete pairs:

>>> readings = ((1, 24), (2, 26))
>>> (2, 26) in readings
True
>>> 2 in readings
False

The value 2 does appear inside the tuple (2, 26), but in does not go looking one level down. It compares against the elements of readings itself, and neither of those two elements is the number 2.

10.2.4 Slicing

Indexing gives you one element. Slicing gives you a run of them. You write two indices separated by a colon inside the square brackets, and Python hands back a new tuple containing the elements from the first index up to — but not including — the second.

>>> fares = (10, 15, 20, 25, 30, 35, 40, 45)
>>> fares[2:5]
(20, 25, 30)

The slice began at index 2 and stopped just before index 5, so it collected the elements at indices 2, 3 and 4. The element at index 5 was left out. This “up to but not including” rule is the single thing to get right about slicing, and Fig. 10.3 shows why it is natural once you think of the indices as marking the cuts between elements rather than the elements themselves.

Fig. 10.3

Fig. 10.3 – Tuple slicing between elements.

Either index may be left out. Omit the first and the slice starts at the beginning; omit the second and it runs to the end; omit both and you get the whole tuple.

>>> fares[:4]
(10, 15, 20, 25)
>>> fares[5:]
(35, 40, 45)
>>> fares[:]
(10, 15, 20, 25, 30, 35, 40, 45)

slicing a tuple

tuple_name[start:stop:step]

Parts: start is the index where the slice begins, and defaults to 0 if omitted; stop is the index just past the last element wanted, and defaults to the end of the tuple; step is the gap between the indices collected, and defaults to 1. Returns / Effect: a new tuple holding the selected elements. The original tuple is untouched. Example:

>>> fares[1:8:2]
(15, 25, 35, 45)

A third number, the step, controls how far the slice jumps each time. With a step of 2 the slice takes every second element.

the slice fares[1:8:2] on the tuple fares = (10, 15, 20, 25, 30, 35, 40, 45) | Step of the slice | Index used | Is index < stop (8)? | Element collected | Result so far | |—|—|—|—|—| | 1 | 1 | yes | 15 | (15,) | | 2 | 3 | yes | 25 | (15, 25) | | 3 | 5 | yes | 35 | (15, 25, 35) | | 4 | 7 | yes | 45 | (15, 25, 35, 45) | | 5 | 9 | no — stop | — | slicing ends |

Result: the slice gives (15, 25, 35, 45). The trace shows that the step is added to the index each time and the slice ends as soon as the index reaches or passes stop, which is why index 8 is never even considered.

Negative indices work in a slice exactly as they do in indexing, counting from the right-hand end:

>>> fares[-3:-1]
(35, 40)

A negative step reverses the direction of travel, and this gives a compact way of reading a tuple backwards:

>>> fares[::-1]
(45, 40, 35, 30, 25, 20, 15, 10)

Q. fares[8] raises an IndexError, yet fares[6:20] does not. Why the difference?

A. Indexing asks for one specific element, and index 8 does not exist, so Python must report a failure. Slicing asks for whatever lies in a range, so an over-long range is simply trimmed to what the tuple actually has — here it returns (40, 45). An out-of-range slice therefore fails quietly, which is convenient but can hide a mistake.

That difference is worth setting out fully, because indexing and slicing are written so similarly that students often expect them to behave the same way.

Table 10.2 – Indexing vs slicing

Feature Indexing Slicing
How it is written One index, fares[2] Two or three values separated by colons, fares[2:5]
What comes back A single element, of whatever type it is A new tuple, even if it holds only one element
Out-of-range value Raises IndexError No error; the range is trimmed to what exists
Result when nothing matches Not possible — it either finds the element or fails Returns the empty tuple ()
Effect on the original tuple None None

Using fares as defined above, predict the result of each of fares[3:3], fares[::3] and fares[-1:-4:-1] before you run them. For any prediction that turns out wrong, write one line saying which part of the slicing rule you had misread.

10.3 Tuple Methods and Built-in Functions

The operators of Section 10.2 handle joining, repeating, testing and slicing. Everything else you are likely to want from a tuple — its length, the largest value in it, how many times some value occurs, the same values in sorted order — is provided by ready-made functions that come with Python. You do not have to write a loop for any of them.

These come in two kinds, and the difference shows up in how they are written. A built-in function is available to the whole language and is called by name, with the tuple handed to it inside brackets, as in len(fares). A method belongs to the tuple itself and is called by writing the tuple’s name, a full stop, and then the method, as in sizes.count('M'). Tuples have only two methods of their own; everything else in this section is a built-in function.

Table 10.3 – Built-in function vs method

Feature Built-in function Method
How it is written len(sizes) — the tuple goes inside the brackets sizes.count('M') — the tuple comes before the full stop
What it belongs to The Python language as a whole The tuple object itself
Works on Many different types — strings, lists, tuples and more Only the type it belongs to
Examples for tuples len(), tuple(), sorted(), min(), max(), sum() count(), index()

Getting this wrong is the commonest small error in this area. Writing sizes.len() fails, because len is not something a tuple owns; writing count(sizes, 'M') fails, because count is not a name the language knows on its own.

Finding the length

len() returns the number of elements in the tuple given to it — not the number of characters, and not the number of values buried inside nested elements. It counts elements at the top level only.

>>> weights = (48, 52, 47, 55, 50)
>>> len(weights)
5

This is the function that tells you the range of valid indices. A tuple of length 5 has indices 0 to 4, so len(weights) - 1 is always the index of the last element.

Building a tuple from another sequence

tuple() creates a tuple. Called with nothing inside the brackets it gives an empty tuple; called with a sequence inside, it takes the elements of that sequence in order and packs them into a new tuple. Fig. 10.4 shows the three sequences you are most likely to convert.

>>> blank = tuple()
>>> blank
()
>>> tuple('bus')
('b', 'u', 's')
>>> tuple([21, 22, 23])
(21, 22, 23)
>>> tuple(range(4))
(0, 1, 2, 3)

Fig. 10.4

Fig. 10.4 – Tuple construction from different sequence types.

A string is a sequence of characters, so converting one gives a tuple with one character per element — a useful reminder that Python treats a string as a sequence in its own right.

Counting and locating an element

count() and index() are the tuple’s own two methods. count() reports how many times a given value occurs, and returns 0 if it does not occur at all. index() reports the position of its first occurrence.

>>> sizes = ('S', 'M', 'L', 'M', 'M', 'XL')
>>> sizes.count('M')
3
>>> sizes.count('XXL')
0
>>> sizes.index('L')
2
>>> sizes.index('M')
1

Notice that sizes.index('M') gives 1 even though 'M' occurs three times. The method stops at the first match and reports that index; the later occurrences are ignored.

The two methods differ sharply in what they do when the value is absent. count() answers calmly with 0, because “none at all” is a perfectly good count. index() cannot answer at all — there is no position to report — so it stops the program with an error.

>>> sizes.index('XXL')
ValueError: tuple.index(x): x not in tuple

Q. How would you look up a position safely, without risking that error?

A. Test first with the membership operator. Check if 'XXL' in sizes: and call sizes.index('XXL') only inside that branch. Asking whether something is there before asking where it is costs one extra line and removes the crash entirely.

Sorting the values

sorted() takes the elements of a tuple and returns them in ascending order. Two things about it need attention, and both are easy to miss.

>>> runs = (38, 91, 12, 67, 45)
>>> sorted(runs)
[12, 38, 45, 67, 91]

The first is that the original is untouched — which must be so, since a tuple cannot be altered:

>>> runs
(38, 91, 12, 67, 45)

The second is that the result is not a tuple. Look at the brackets in the output above: sorted() always hands back a list, whatever kind of sequence it was given. If you need a tuple back, convert the result with tuple().

expecting sorted() to return a tuple What students write:

>>> stops = ('Tonk', 'Ajmer', 'Kota', 'Alwar')
>>> ordered = sorted(stops)
>>> ordered + ('Dausa',)

What happens:

TypeError: can only concatenate list (not "tuple") to list

The fix: ordered = tuple(sorted(stops)). The inner call sorts and gives a list; the outer call turns that list back into a tuple, after which tuple operations work as expected.

Sorting is not limited to numbers. Strings sort into dictionary order, comparing character by character from the left:

>>> stops = ('Tonk', 'Ajmer', 'Kota', 'Alwar')
>>> sorted(stops)
['Ajmer', 'Alwar', 'Kota', 'Tonk']
Smallest, largest and total

min() and max() return the smallest and the largest element of a tuple, and sum() returns the total of its elements.

>>> runs = (38, 91, 12, 67, 45)
>>> min(runs)
12
>>> max(runs)
91
>>> sum(runs)
253

min() and max() also work on a tuple of strings, where “smallest” means first in dictionary order and “largest” means last. sum(), however, is arithmetic through and through, and refuses anything it cannot add:

>>> sum(stops)
TypeError: unsupported operand type(s) for +: 'int' and 'str'

All three need elements that can be compared or added with one another. A tuple mixing numbers and strings will therefore defeat min() and max() as well, because Python has no rule for deciding whether 10 comes before or after 'Kota'.

Table 10.4 – Built-in functions and methods for tuples, with what each one returns

Name Kind What it does Returns
len() Built-in function Counts the elements of the tuple at the top level An integer
tuple() Built-in function Builds a tuple, empty or from a given sequence A tuple
count() Method Counts how many times a value occurs in the tuple An integer, 0 if absent
index() Method Finds the position of the first occurrence of a value An integer; raises ValueError if absent
sorted() Built-in function Arranges the elements in ascending order A new list, leaving the tuple unchanged
min() Built-in function Finds the smallest element One element of the tuple
max() Built-in function Finds the largest element One element of the tuple
sum() Built-in function Adds all the elements A number
Putting them to work

A single problem is enough to show how these fit together. The task below is a real one from any class register: given a week’s attendance for one student, report the totals and say when the first absence happened.

reporting attendance from a record of daily marks Input: a tuple holding one mark per working day, 'P' for present and 'A' for absent. Output: the number of working days, the days present, the days absent, and the day number of the first absence. Method:

1. Take the tuple of daily attendance marks.
2. Count the elements of the tuple to get the number of working days.
3. Count the occurrences of 'P' to get the days present.
4. Count the occurrences of 'A' to get the days absent.
5. Find the index of the first 'A' and add 1 to convert the index into a day number.
6. Display all four results.

Why it works: every element of the tuple is one working day, so counting elements counts days, and counting a particular mark counts the days carrying that mark. Step 5 needs the + 1 because indices begin at 0 while day numbers begin at 1. Watch out: if the student was present on every day, step 5 has no first 'A' to find and index() raises ValueError. The record must be checked for an absence before that step is attempted.

report attendance totals for one student from a week’s record Problem: from a fixed tuple of daily attendance marks, display the total working days, the days present, the days absent, and the day number on which the student was first absent.

# To report attendance details for one student from a week's record

attendance = ('P', 'P', 'A', 'P', 'A', 'P')

total_days = len(attendance)
present_days = attendance.count('P')
absent_days = attendance.count('A')

print("Total working days:", total_days)
print("Days present:", present_days)
print("Days absent:", absent_days)

if 'A' in attendance:
    first_absent = attendance.index('A') + 1
    print("First absence on day:", first_absent)
else:
    print("No absence recorded")

Sample output:

Total working days: 6
Days present: 4
Days absent: 2
First absence on day: 3

Identifier description:

Table 10.5 – Identifiers used in the attendance program

Identifier Type Purpose
attendance Tuple of strings Holds one attendance mark per working day, in day order
total_days Integer Number of working days in the record
present_days Integer Count of the mark 'P' in the record
absent_days Integer Count of the mark 'A' in the record
first_absent Integer Day number of the first absence, obtained from the index plus one

Notes: the record is stored in a tuple rather than a list because an attendance register, once written, should not be altered by the program that reads it. The membership test before index() is what keeps the program safe on a record with no absence at all — the case the algorithm warned about.

Change the tuple in the program above to ('P', 'P', 'P', 'P', 'P') and run it again. Say in one line which branch runs and why, and what would have happened had the if test not been there.

10.4 Tuple Assignment

A library register keeps three facts about every book that goes out: an accession code, a title, and the number of days it may be kept. Those three facts belong together, so a tuple is the natural place to hold them. But the moment you want to work with them separately, you need each fact under its own name — and copying them out one index at a time is tedious:

>>> issue = ('L-207', 'Bhugol Darshan', 14)
>>> code = issue[0]
>>> title = issue[1]
>>> days = issue[2]

Python offers a shorter route. Write a tuple of variable names on the left of the assignment operator and a tuple of values on the right, and each name takes the value standing in the matching position. This is called tuple assignment.

>>> (code, title, days) = ('L-207', 'Bhugol Darshan', 14)
>>> code
'L-207'
>>> title
'Bhugol Darshan'
>>> days
14

One line has done the work of three, and it reads far closer to the intention: these three names describe one record. The matching is strictly by position, as Fig. 10.5 shows — the first name takes the first value, the second name the second, and so on. Names are never matched by meaning or by type.

Fig. 10.5

Fig. 10.5 – Tuple assignment matching by position.

NoteDefinition

(Tuple assignment is the assignment of a tuple of values to a tuple of variables in a single statement, in which each variable receives the value occupying the same position, and the number of variables must equal the number of values.)

The brackets on either side may be dropped, because — as Section 10.1.1 established — it is the comma that makes a tuple. The following line does exactly the same work as the one above:

>>> code, title, days = 'L-207', 'Bhugol Darshan', 14
Packing and unpacking

Two things are really happening in that statement, and it helps to see them as separate steps. First the values on the right are gathered into a single tuple; this is called packing. Then that tuple is opened out and its elements handed to the names on the left; this is called unpacking.

>>> record = 'L-311', 'Ganit ke Prashna', 7
>>> record
('L-311', 'Ganit ke Prashna', 7)

Here only packing took place — one name on the left, so the tuple stayed whole. Unpacking is what happens when the left side carries several names.

Table 10.6 – Packing vs unpacking

Feature Packing Unpacking
Where it happens On the right of the assignment On the left of the assignment
What it does Gathers several values into one tuple Distributes the elements of a tuple among several names
Written as record = 'L-311', 'Ganit ke Prashna', 7 code, title, days = record
Number of names involved One As many as the tuple has elements
Result A single tuple object Several ordinary variables

Because unpacking works on any sequence of the right length, the value on the right need not have been written as a tuple at all. A list unpacks just as readily:

>>> p, q = [3, 7]
>>> p
3
>>> q
7
The counts must match

Unpacking has one strict condition: there must be exactly as many names on the left as there are elements on the right. Python will not silently drop a value or leave a name empty, so a mismatch in either direction stops the program.

>>> (a, b, c) = (12, 18)
ValueError: not enough values to unpack (expected 3, got 2)
>>> (a, b) = (12, 18, 24)
ValueError: too many values to unpack (expected 2)

The two messages are worth reading rather than glancing at. The first says the left side asked for three and only two arrived; the second says two names could not absorb three values. Between them they tell you which side to correct.

Q. x = 12, 18 runs without complaint, but x, y, z = 12, 18 does not. Why does the first escape the counting rule?

A. The first is packing, not unpacking. One name on the left simply receives the whole tuple (12, 18), so there is nothing to distribute and no counts to match. The rule applies only when the left side has more than one name.

The right side is evaluated first

The values on the right need not be plain literals. Any expression may appear there, and Python evaluates every one of them, builds the resulting tuple, and only then assigns.

>>> length, breadth = 8, 5
>>> area, perimeter = length * breadth, 2 * (length + breadth)
>>> area
40
>>> perimeter
26

the statement area, perimeter = length * breadth, 2 * (length + breadth) with length = 8 and breadth = 5 | Stage | What Python does | Value at this stage | |—|—|—| | 1 | Evaluates the first expression on the right | 8 * 5 gives 40 | | 2 | Evaluates the second expression on the right | 2 * (8 + 5) gives 26 | | 3 | Packs both results into one tuple | (40, 26) | | 4 | Unpacks that tuple to the names on the left | area = 40, perimeter = 26 |

Result: area holds 40 and perimeter holds 26. The trace shows that no name on the left receives anything until every expression on the right has already been worked out.

That ordering is not a detail — it has a consequence you will use in Section 10.6. Since the whole right side is built before any name on the left is touched, an assignment may safely mention the very names it is about to overwrite. Whatever those names held is captured into the tuple first, and the old values are still available at the moment they are read.

Write a single tuple assignment that gives hours the value 6, minutes the value 45, and label the string 'morning'. Then attempt the same assignment with only two values on the right, note the exact error message, and say which of the two mismatch messages above it matches.

10.5 Nested Tuples

A single tuple holds one record neatly — a route code, a destination and a fare, kept together and safe from change. But a bus depot does not have one route; it has a chart of them. What is needed now is a container holding several records at once, each record still whole and still in order.

The answer requires nothing new. An element of a tuple may itself be a tuple, so a chart of four routes is simply a tuple with four elements, each of which is a three-element tuple. A tuple written inside another tuple in this way is called a nested tuple.

NoteDefinition

(A nested tuple is a tuple in which one or more elements are themselves tuples.)

Consider the fare chart in Table 10.7. Each row is one route, and the three columns are the same three facts for every route — which is exactly the shape a nested tuple stores.

Table 10.7 – Fare chart of four bus routes, holding the route code, the destination and the fare in rupees

Route code Destination Fare
R1 Amer 25
R2 Bagru 30
R3 Chomu 45
R4 Dausa 40

Written as a nested tuple, that chart becomes:

>>> routes = (('R1', 'Amer', 25), ('R2', 'Bagru', 30), ('R3', 'Chomu', 45))
>>> len(routes)
3

The length is 3, not 9. Section 10.3 said that len() counts elements at the top level only, and here the top level holds three things — three tuples. The nine individual values sit one level down and are not counted.

Fig. 10.6

Fig. 10.6 – Indexing a nested tuple.

Reaching a value inside a nested tuple

A single index picks out a whole record, because that record is one element of the outer tuple:

>>> routes[1]
('R2', 'Bagru', 30)

To go further in, add a second index. The first index chooses the record; the second chooses the field within that record. Python applies them from left to right, so routes[1][1] means “take element 1 of routes, then take element 1 of whatever that gave”.

>>> routes[1][1]
'Bagru'
>>> routes[2][2]
45
>>> routes[0][-1]
25

Negative indices, slicing and the membership operator all continue to work at either level, because each level is an ordinary tuple in its own right.

Q. routes[1][1] gives the string 'Bagru'. What would routes[1][1][0] give?

A. 'B'. The third index is applied to the string, and a string is itself a sequence, so index 0 of 'Bagru' is its first character. Indexing keeps going as long as what it is applied to is a sequence.

using one index where two are needed What students write:

>>> routes[1] + 5

What happens:

TypeError: can only concatenate tuple (not "int") to tuple

The fix: write routes[1][2] + 5. A single index still leaves you holding a whole record, not a number. Whenever an operation complains about a tuple where a value was expected, an index has usually been left out.

Traversing a nested tuple

To work through every record, use a for loop. There are two ways to write it, and both are worth knowing.

The first runs over the index positions, using range() with the length of the tuple, and reaches each field by double indexing:

>>> for i in range(len(routes)):
        print(routes[i][0], routes[i][1])

R1 Amer
R2 Bagru
R3 Chomu

The second takes each record directly and unpacks it into named variables as it goes — precisely the tuple assignment of Section 10.4, done once per turn of the loop:

>>> for code, place, fare in routes:
        print(code, place)

R1 Amer
R2 Bagru
R3 Chomu

Both produce the same output. The second is usually the better program, because code, place and fare say what each value means, whereas routes[i][2] leaves the reader counting positions. Its one requirement is that every record must have exactly the number of fields being unpacked — a record with a missing field would raise the ValueError you met in Section 10.4.

Table 10.8 – Traversing a nested tuple by index vs by unpacking

Feature By index By unpacking
Loop written as for i in range(len(routes)): for code, place, fare in routes:
Reaching a field routes[i][2] fare
Readability The reader must remember what position 2 holds The name states what the value is
Requirement None beyond a valid index Every record must have the same number of fields
Also gives you The position number i, useful for serial numbering No position number unless one is counted separately
A fare chart in practice

The problem below uses both ideas — unpacking to display the chart, and double indexing to compare one field across records.

displaying a fare chart and finding the costliest route Input: a nested tuple in which each element is a record holding a route code, a destination and a fare. Output: the full chart displayed as rows, followed by the highest fare and the route that charges it. Method:

1. Take the nested tuple of route records.
2. Display a heading row.
3. For each record in the tuple, unpack it into code, destination and fare, and display the three values.
4. Assume the first record is the costliest so far.
5. For each record in the tuple, compare its fare with the fare of the costliest record so far.
6. If the record's fare is greater, treat that record as the costliest so far.
7. After all records have been examined, display the fare and the code of the costliest record.

Why it works: step 6 keeps the running answer correct at every point — after examining any number of records, the record held is the costliest among exactly those examined. When the loop ends, all records have been examined, so the record held is the costliest overall. Watch out: step 4 assumes there is at least one record. On an empty tuple there is no first record to start from, and the program would fail at that step.

display a bus fare chart and report the costliest route Problem: from a nested tuple of route records, display the chart with a heading row, then display the highest fare and the code of the route charging it.

# To display a fare chart from a nested tuple and report the costliest route

routes = (('R1', 'Amer', 25),
          ('R2', 'Bagru', 30),
          ('R3', 'Chomu', 45),
          ('R4', 'Dausa', 40))

print("Route", "Destination", "Fare", sep="\t")
for code, place, fare in routes:
    print(code, place, fare, sep="\t")

costliest = routes[0]
for record in routes:
    if record[2] > costliest[2]:
        costliest = record

print("Highest fare:", costliest[2], "on route", costliest[0])

Sample output:

Route   Destination Fare
R1  Amer    25
R2  Bagru   30
R3  Chomu   45
R4  Dausa   40
Highest fare: 45 on route R3

Identifier description:

Table 10.9 – Identifiers used in the fare chart program

Identifier Type Purpose
routes Nested tuple Holds one record per route, each with code, destination and fare
code String Route code of the record currently being displayed
place String Destination of the record currently being displayed
fare Integer Fare of the record currently being displayed
costliest Tuple The record with the highest fare found so far
record Tuple The record currently being compared in the second loop

Notes: the two loops do different jobs and are deliberately written differently. The display loop unpacks, because it needs all three fields under readable names; the comparison loop keeps each record whole, because when a new maximum is found the whole record must be remembered, not just the fare. Storing the chart as a tuple rather than a list also guarantees that the display loop cannot accidentally alter a fare while printing it.

the second loop of the program, finding the costliest route | Turn | record | record[2] | Is it greater than costliest[2]? | costliest after this turn | |—|—|—|—|—| | Start | — | — | — | (‘R1’, ‘Amer’, 25) | | 1 | (‘R1’, ‘Amer’, 25) | 25 | 25 > 25 — no | (‘R1’, ‘Amer’, 25) | | 2 | (‘R2’, ‘Bagru’, 30) | 30 | 30 > 25 — yes | (‘R2’, ‘Bagru’, 30) | | 3 | (‘R3’, ‘Chomu’, 45) | 45 | 45 > 30 — yes | (‘R3’, ‘Chomu’, 45) | | 4 | (‘R4’, ‘Dausa’, 40) | 40 | 40 > 45 — no | (‘R3’, ‘Chomu’, 45) |

Result: the loop ends holding ('R3', 'Chomu', 45), and the program prints Highest fare: 45 on route R3. The trace shows why the last record examined is not the answer — the running maximum survives every later comparison it wins.

\t inside a string is not two characters but one: the tab character, which moves the output to the next tab stop and is what lines the columns up above. Its companion \n starts a new line. Both are called escape characters, and both are counted as a single character by len().

Add a fifth record ('R5', 'Kotputli', 45) to the tuple in the program and run it again. The chart will now hold two routes at the highest fare. Say which route the program reports, and point to the exact line that decides it.

10.6 Tuple Handling

Everything the tuple can do has now been laid out — how it is built, how it is read, what it refuses, and what the built-in functions will do for you. This section puts those pieces to work on three complete problems. Each one is a task that a tuple solves more neatly than any other type would, and each is worked the same way: the problem first, then the method, then the program, and only then the code running.

Exchanging two values

Swapping the contents of two variables is one of the oldest small problems in programming, and the usual solution needs a third variable to hold one value while the other is moved. Tuple assignment removes that need entirely, for the reason established at the end of Section 10.4 — the whole right-hand side is packed into a tuple before any name on the left is touched, so both old values are safely captured before either is overwritten.

exchanging the values of two variables without a third variable Input: two numbers, read from the user. Output: the two numbers displayed before and after the exchange. Method:

1. Read the first number and the second number.
2. Display both numbers as they stand.
3. Pack the second number and the first number, in that order, into a tuple.
4. Unpack that tuple into the first and the second variable.
5. Display both numbers again.

Why it works: step 3 finishes before step 4 begins, so the tuple already holds both original values by the time either variable is reassigned. Neither value can be lost by being overwritten too early. Watch out: the same reasoning fails if the exchange is written as two separate assignment statements, because the first of them destroys one value before the second can read it.

exchange the values held by two variables Problem: read two counts from the user, display them, exchange them without using a third variable, and display them again.

# To exchange the values held by two variables using tuple assignment

first = int(input("Enter the count at counter 1: "))
second = int(input("Enter the count at counter 2: "))

print("Before exchange - counter 1:", first, "counter 2:", second)

first, second = second, first

print("After exchange - counter 1:", first, "counter 2:", second)

Sample output:

Enter the count at counter 1: 18
Enter the count at counter 2: 25
Before exchange - counter 1: 18 counter 2: 25
After exchange - counter 1: 25 counter 2: 18

Identifier description:

Table 10.10 – Identifiers used in the exchange program

Identifier Type Purpose
first Integer Count read from the first counter, and afterwards the exchanged value
second Integer Count read from the second counter, and afterwards the exchanged value

Notes: no tuple is ever stored under a name here. One is built by the right-hand side, used immediately by the left, and then discarded — which is the whole mechanism of the swap. The line also reads as a statement of intent rather than as a sequence of moves, and that is its real advantage over the three-variable version.

Q. Would first = second followed by second = first do the same job?

A. No. The first statement overwrites first, so by the time the second statement runs the original value of first no longer exists anywhere, and both variables end up holding the second count. The single tuple assignment avoids this precisely because it reads both values before writing either.

Returning more than one value from a function

A function returns one value. That sounds like a restriction until you notice that the one value returned may be a tuple holding as many results as you like — and the caller can unpack it straight into separate names.

The problem below needs two results from one calculation: a duration given in minutes has to be reported as whole hours plus the minutes left over.

splitting a duration in minutes into hours and minutes Input: a whole number of minutes. Output: the equivalent number of whole hours, and the minutes remaining. Method:

1. Take the total number of minutes.
2. Divide the total by 60 using integer division to obtain the whole hours.
3. Take the remainder of the total on division by 60 to obtain the minutes left over.
4. Return the hours and the minutes together as a tuple.

Why it works: integer division discards the fraction, so it yields exactly the number of complete hours contained in the total; the remainder is by definition what is left after those complete hours are removed. Together the two account for the whole total. Watch out: the method assumes a whole number of minutes. A total entered with a decimal point would make the integer division produce a floating-point result, and the report would read oddly.

report a duration in minutes as hours and minutes Problem: read a duration in minutes and display it as whole hours and the remaining minutes, using a function that returns both results.

# To convert a duration in minutes into hours and remaining minutes

def split_time(total):
    hours = total // 60
    minutes = total % 60
    return (hours, minutes)

duration = int(input("Enter the duration in minutes: "))
h, m = split_time(duration)

print("That is", h, "hour(s) and", m, "minute(s)")

Sample output:

Enter the duration in minutes: 145
That is 2 hour(s) and 25 minute(s)

Identifier description:

Table 10.11 – Identifiers used in the duration program

Identifier Type Purpose
total Integer Parameter of the function, holding the minutes passed to it
hours Integer Whole hours contained in the total, computed inside the function
minutes Integer Minutes left over after the whole hours, computed inside the function
duration Integer Duration read from the user
h Integer Hours received back from the function
m Integer Minutes received back from the function

Notes: the function still returns a single object — one tuple — so nothing about Python’s rule has been bent. The unpacking happens at the call, where h, m = split_time(duration) distributes the two elements into two names. Returning the pair as a tuple also keeps the two results together and in a fixed order, which is safer than returning a list that a later line could quietly rearrange.

Collecting user input into a tuple

The last problem builds a tuple whose contents are not known in advance. Since a tuple cannot grow in place, each new value is added by concatenation, exactly as in Section 10.2.1 — a longer tuple is built each time and the name is repointed to it.

reading n values into a tuple and summarising them Input: a count n, followed by n numbers entered one at a time. Output: the tuple of all values entered, together with the highest, the lowest and the average. Method:

1. Start with an empty tuple.
2. Read how many values are to be entered.
3. Repeat the following n times:
       read one value from the user;
       build a new tuple by joining the current tuple to a one-element tuple holding that value.
4. Display the completed tuple.
5. Display the largest element, the smallest element, and the total divided by the number of elements.

Why it works: each turn of the loop leaves the tuple exactly one element longer and preserves the order of entry, so after n turns the tuple holds all n values in the order they were typed. The summary functions then work on a complete tuple, not a partial one. Watch out: if the user enters 0 for the count, the tuple stays empty, and step 5 fails — max() has nothing to compare, and the average would divide by zero.

read daily temperature readings into a tuple and summarise them Problem: read a count and then that many temperature readings, store them in a tuple, and display the tuple along with the highest reading, the lowest reading and the average.

# To read n daily temperature readings into a tuple and summarise them

readings = tuple()
n = int(input("How many readings? "))

for i in range(n):
    value = int(input("Enter reading " + str(i + 1) + ": "))
    readings = readings + (value,)

print("Readings:", readings)
print("Highest:", max(readings))
print("Lowest:", min(readings))
print("Average:", sum(readings) / len(readings))

Sample output:

How many readings? 5
Enter reading 1: 31
Enter reading 2: 28
Enter reading 3: 35
Enter reading 4: 30
Enter reading 5: 26
Readings: (31, 28, 35, 30, 26)
Highest: 35
Lowest: 26
Average: 30.0

Identifier description:

Table 10.12 – Identifiers used in the readings program

Identifier Type Purpose
readings Tuple of integers Holds every reading entered so far, in order of entry
n Integer Number of readings the user intends to enter
i Integer Loop counter, used to number the prompt shown to the user
value Integer The single reading read during the current turn of the loop

Notes: the average prints as 30.0 rather than 30 because the division operator / always produces a floating-point result, whatever the two numbers are. The prompt is assembled with str(i + 1) so that the readings are numbered from 1 for the user while the loop counter runs from 0 inside the program.

the input loop of the readings program, for n = 5 and the readings 31, 28, 35, 30 and 26 | Turn | i | value read | readings after the turn | |—|—|—|—| | Start | — | — | () | | 1 | 0 | 31 | (31,) | | 2 | 1 | 28 | (31, 28) | | 3 | 2 | 35 | (31, 28, 35) | | 4 | 3 | 30 | (31, 28, 35, 30) | | 5 | 4 | 26 | (31, 28, 35, 30, 26) |

Result: the loop ends with readings holding (31, 28, 35, 30, 26). The trace also shows that no tuple is ever modified — a longer tuple is built on each turn and the name readings is made to refer to it, which is why every row of the last column shows a different object.

trying to add to a tuple the way you would add to a list What students write:

readings.append(value)

What happens:

AttributeError: 'tuple' object has no attribute 'append'

The fix: write readings = readings + (value,). A tuple has no append() because appending would mean changing it, which it does not allow. The concatenation builds a new tuple instead, and the assignment is what makes the name refer to the longer one.

Practical link: this task exercises tuple creation, traversal and the built-in functions of Sections 10.3 to 10.6, and is filed under Python programs in the report file. Write a program that reads the marks of n students of a class into a tuple, then displays the tuple, the highest and lowest marks, the class average, and the number of students who scored above that average. The count and the marks are to be read from the user. Test it with the marks 55, 72, 41, 88, 63 and 72, and check the count it reports; then run it again for a single student and say whether the program still behaves sensibly.

10.7 Introduction to Dictionaries

Store a class’s marks in a tuple and a difficulty appears at once. The tuple (78, 91, 66, 84) records four marks perfectly well, but it says nothing about whose they are. You would have to keep a second tuple of names alongside it, find the name first, note its index, and then use that index on the marks. Two containers, kept in step by hand, and a search before every lookup.

What you actually want is to look up a mark the way you look up a word in a dictionary — by the thing that names it, not by its position on the page. Python provides a type built for exactly this. A dictionary stores data as pairs: a key that identifies, and a value that is identified.

>>> scores = {'Meera': 78, 'Arun': 91, 'Kavya': 66, 'Devraj': 84}
>>> scores['Arun']
91

One container, no second tuple, and no search written by you. The pairs are held inside curly braces, a colon joins each key to its value, and commas separate one pair from the next. A key together with its value is called an item.

NoteDefinition

(A dictionary is a collection of key-value pairs, written within curly braces, in which each key is unique and is used to access its associated value; the keys must be of an immutable type, while the values may be of any type and may repeat.)

Because a value is reached through its key rather than through a position, a dictionary is called a mapping — it maps each key to one value. This is a different idea from the sequences of the first half of this chapter, and Fig. 10.7 contrasts the two.

Fig. 10.7

Fig. 10.7 – Sequence vs Mapping.

the mapping type Statement: a mapping stores pairs of a key and a value, and retrieves a value directly from its key rather than by examining the stored items one after another. In words: you hand the collection the key, and it goes straight to the matching value instead of hunting for it. Why it holds: the key is converted into a number that tells Python where in memory that item is kept. The same key always converts to the same number, so the item can be reached without looking at any other item. This is why the key must be of an immutable type — if a key could change after it was stored, the number computed from it would no longer point where the item actually is, and the item would be lost. Used for: any data that is naturally identified by a label rather than by a position — marks by student name, fares by route code, stock by item code, a word count by the word itself. Watch out: the speed comes from lookup by key alone. A dictionary offers no quick way to search by value; finding which key holds the mark 91 still means examining the items one by one.

Q4. A dictionary is not searched by position. So when you ask it for one value out of a thousand, how does it know where to look?

A. It computes the location from the key itself. The key is converted into a number that identifies where its item is stored, so Python goes straight there instead of comparing the key against the stored items one after another. This is also why keys must be of an immutable type: a key that could change would no longer lead back to the place where its own item was put.

10.7.1 Creating a Dictionary

An empty dictionary is written as a pair of curly braces with nothing inside, or built by the dict() function.

>>> stock = {}
>>> stock
{}
>>> type(stock)
<class 'dict'>
>>> stock = dict()
>>> stock
{}

Note that curly braces, unlike round brackets, have no other job in this position, so there is no comma trap here — {} really is an empty dictionary.

A dictionary with items in it is written by listing the pairs, each as key: value:

>>> book = {'title': 'Vigyan Prayog', 'copies': 3, 'price': 245.75}
>>> book['price']
245.75

This second example is worth pausing on. In scores the keys were student names and every value was a mark, so all the values were of one kind. In book the keys are field names and the values are of three different types — a string, an integer and a float. Both uses are perfectly ordinary: a dictionary can hold many records of the same kind, or all the fields of a single record.

creating a dictionary

dict_name = {key1: value1, key2: value2, key3: value3}

Parts: dict_name is any valid variable name; key1, key2, key3 are the identifying keys, which must be unique and of an immutable type; value1, value2, value3 are the values they map to; the colon joins a key to its value and the comma separates one item from the next. Returns / Effect: a dictionary object is built and the name on the left is made to refer to it. Example:

>>> fare = {'R1': 25, 'R2': 30, 'R3': 45}
>>> fare['R3']
45
What a key may be, and what it may not

A key must be of an immutable type — a number, a string or a tuple. This is the rule the Theory Snapshot explained: the storage position is computed from the key, so a key that could change from underneath would strand its own item. A list, being mutable, is refused outright.

>>> location = {(26, 75): 'Amer'}
>>> location[(26, 75)]
'Amer'
>>> location = {[26, 75]: 'Amer'}
TypeError: unhashable type: 'list'

The tuple works as a key precisely because it cannot be altered — one more use for the immutability of Section 10.1.3.

Keys must also be unique. If the same key is written twice while a dictionary is being created, no error is reported; the later value simply replaces the earlier one, and only one item survives.

>>> scores = {'Meera': 78, 'Arun': 91, 'Meera': 82}
>>> scores
{'Meera': 82, 'Arun': 91}

Values are under no such restrictions. They may repeat freely, and may be of any type at all — including a list, a tuple, or another dictionary.

>>> fee = {'Meera': 1200, 'Arun': 1200, 'Kavya': 1200}
>>> len(fee)
3

Three students, one fee amount, three separate items. It is the keys that must differ, never the values.

From Python 3.7 onwards a dictionary keeps its items in the order in which they were inserted, so displaying one gives the pairs back in the order you wrote them. That order is a convenience only. It is not a position you can index by, and code that depends on it in older versions of Python will not behave the same way.

10.7.2 Accessing Items in a Dictionary

To read a value, write the key inside square brackets after the dictionary’s name. The notation is borrowed from indexing, but what goes inside the brackets is the key, not a position.

>>> scores = {'Meera': 78, 'Arun': 91, 'Kavya': 66, 'Devraj': 84}
>>> scores['Kavya']
66
>>> scores['Devraj']
84

Where the item sits in the dictionary makes no difference whatever. 'Kavya' maps to 66 and will go on doing so however the dictionary is later rearranged or added to. This is the real gain over the two-tuple arrangement described at the start of this section: a name and its mark are bound together, and cannot drift apart.

If the key is not present, Python cannot invent a value, so it stops with an error naming the key that failed:

>>> scores['Suhani']
KeyError: 'Suhani'

The same happens if you forget that a dictionary is not a sequence and try to use a position:

>>> scores[0]
KeyError: 0

Read that message closely. Python has not complained that 0 is out of range, as a tuple would. It has taken 0 as a perfectly acceptable key, looked for it, and reported that no item has that key. Had the dictionary genuinely been built with integer keys — {0: 'Sun', 1: 'Mon'}, say — then scores[0] would have returned a value. In a dictionary, an integer inside the brackets is a key like any other.

Table 10.13 – Accessing a tuple vs accessing a dictionary

Feature Tuple Dictionary
Written as temp[2] scores['Kavya']
What goes in the brackets A position, counted from 0 A key
What identifies an element Where it sits in the sequence The key stored with it
Effect of rearranging the collection Every element’s index changes Nothing changes; keys still map to the same values
When what is asked for does not exist IndexError KeyError
Duplicates allowed Elements may repeat freely Keys must be unique; values may repeat

Q. A dictionary of marks is displayed and 'Arun': 91 appears second. Does scores[1] therefore give 91?

A. No — it raises KeyError: 1, unless the dictionary happens to have an item whose key is the integer 1. Being displayed second is not a position you can ask for; there is no way to reach an item of a dictionary except through its key.

Build the dictionary fare = {'R1': 25, 'R2': 30, 'R3': 45} and, before running anything, write down what you expect from each of fare['R2'], fare[1] and fare['r3']. Run all three and account for any answer that surprised you, paying attention to the exact spelling and casing of the key.

10.8 Dictionaries are Mutable

A stock register is not written once and closed. Items are sold, fresh stock arrives, a line is struck off when a product is discontinued. Data of this kind has to be editable, and here the dictionary differs completely from the tuple: a dictionary is mutable, so items can be added to it, changed in it and removed from it after it has been created.

The difference goes deeper than convenience. When a tuple appeared to change in Section 10.1.3, a new tuple was built and the name repointed. A dictionary genuinely changes in place — it is the same dictionary object throughout, with different contents.

>>> stock = {'pen': 40, 'pencil': 25, 'eraser': 60}
>>> stock['pencil'] = 30
>>> stock
{'pen': 40, 'pencil': 30, 'eraser': 60}

Fig. 10.8 shows the three edits this section covers, each acting on that same dictionary.

Fig. 10.8

Fig. 10.8 – Adding, modifying and deleting dictionary items.

10.8.1 Adding a New Item

To add an item, write the new key inside square brackets and assign a value to it. The dictionary grows by one item.

>>> stock = {'pen': 40, 'pencil': 25, 'eraser': 60}
>>> stock['scale'] = 15
>>> stock
{'pen': 40, 'pencil': 25, 'eraser': 60, 'scale': 15}
>>> len(stock)
4

There is no method to call and no position to specify. Because items are reached by key and not by position, “where” the new item goes is not a question that needs answering — the key alone is enough to find it again.

10.8.2 Modifying an Existing Item

To change the value of an item, write its existing key inside square brackets and assign the new value. The old value is overwritten and the dictionary’s length does not change.

>>> stock['pencil'] = 30
>>> stock
{'pen': 40, 'pencil': 30, 'eraser': 60, 'scale': 15}
>>> len(stock)
4

The new value is often worked out from the old one. Selling five pens means reading the current count, subtracting from it, and storing the result back under the same key:

>>> stock['pen'] = stock['pen'] - 5
>>> stock['pen']
35

Notice that adding and modifying are written identically. The same statement does one or the other depending on a single fact: whether the key already exists. If it does, the value is replaced; if it does not, a new item is created. Python does not ask which you intended.

Table 10.14 – Adding, modifying and deleting an item

Feature Adding Modifying Deleting
Written as stock['scale'] = 15 stock['pencil'] = 30 del stock['eraser']
Condition on the key Must not already be present Must already be present Must already be present
Effect on the length Increases by one Unchanged Decreases by one
If the key is absent This is the normal case The item is created instead, with no warning Raises KeyError
Effect on the dictionary object Changed in place Changed in place Changed in place

That third row is where trouble hides. A deletion of a key that is not there fails loudly, but a modification of a key that is not there fails silently — it quietly becomes an addition.

a misspelt key while updating a value What students write:

>>> stock = {'pen': 40, 'pencil': 25}
>>> stock['Pencil'] = 30
>>> stock
{'pen': 40, 'pencil': 25, 'Pencil': 30}

What happens: no error at all. 'Pencil' and 'pencil' are different keys, so instead of updating the pencil count, a second, unrelated item has been created. The register now claims to stock two different things, and the original value was never touched. The fix: check the key before assigning, either by eye or with the membership test of Section 10.9 — if 'Pencil' in stock:. Keys are compared character by character, so casing and spacing must match exactly.

Q. How can you tell, after running stock['scale'] = 15, whether you added an item or overwrote one?

A. Compare len(stock) before and after. If the length rose by one it was an addition; if it stayed the same, a value was replaced. The statement itself gives no clue, which is exactly why the misspelt-key error above goes unnoticed.

10.8.3 Deleting an Existing Item

To remove an item, use the del statement with the key of the item to be removed. Both the key and its value go, and the dictionary shrinks by one.

>>> stock = {'pen': 35, 'pencil': 30, 'eraser': 60, 'scale': 15}
>>> del stock['eraser']
>>> stock
{'pen': 35, 'pencil': 30, 'scale': 15}

del is not a method of the dictionary. It is a statement of the Python language, which is why it is written before the item rather than after a full stop — there are no brackets to call, and nothing is returned.

A key that is not present cannot be removed, and Python says so:

>>> del stock['marker']
KeyError: 'marker'

The same statement, given the dictionary’s name with no key after it, removes the whole dictionary from memory. The name itself ceases to exist, so any later use of it is an error of a different kind:

>>> del stock
>>> stock
NameError: name 'stock' is not defined

The distinction between these two forms matters. del stock['eraser'] empties one line of the register; del stock throws the register away. The error messages tell them apart afterwards — a KeyError means the dictionary is there but the key is not, while a NameError means there is no dictionary any more.

Build the dictionary stock = {'pen': 40, 'pencil': 25, 'eraser': 60}. Now, one line at a time and checking len(stock) after each, add an item for 'sharpener', reduce the pen count by 12, and delete the eraser item. Then attempt to delete 'eraser' a second time and write down the message you get.

10.9 Dictionary Operations

The operators that worked on tuples do not all carry across. Joining two dictionaries with + is meaningless, because there are no positions to lay end to end, and repeating a dictionary with * is meaningless too, since keys must stay unique. Slicing needs a range of positions, and a dictionary has none.

One operator does carry across, and it is the one that matters most in everyday use: the membership operator.

10.9.1 Membership

The in operator tells you whether a dictionary contains a given key, returning True if it does and False if it does not. Its partner not in reverses the answer. The register below records which student currently holds each library book.

>>> issued = {'L-207': 'Meera', 'L-311': 'Arun', 'L-405': 'Kavya'}
>>> 'L-311' in issued
True
>>> 'L-500' in issued
False
>>> 'L-500' not in issued
True

The whole point of the operator lies in what it examines. It looks at the keys and only at the keys — the values are never compared at all. So a student’s name, which appears in this dictionary as plainly as any book code does, is not found:

>>> 'Meera' in issued
False

That answer is correct, not a quirk. 'Meera' is a value here, and in is asking whether any item is identified by 'Meera'. None is. Fig. 10.9 shows which column of the register the operator actually reads.

Fig. 10.9

Fig. 10.9 – The in operator on a dictionary.

Table 10.15 – The in operator on a tuple and on a dictionary

Feature On a tuple On a dictionary
What is compared The elements of the tuple The keys only
Written as 'Maths' in subjects 'L-311' in issued
What it returns True or False True or False
How the answer is found The elements are examined one after another The key is looked up directly
Searching by anything else Not applicable — a tuple has only elements A value cannot be found with in

The fourth row explains why membership is worth using on a dictionary even when speed seems irrelevant. On a tuple the operator has to work through the elements until it finds a match or runs out; on a dictionary it computes the location from the key, exactly as a lookup does, so the size of the dictionary makes almost no difference to how long the test takes.

Testing before looking up

The commonest use of in is not to answer a question for its own sake but to make a lookup safe. Section 10.7.2 showed that asking for a key that does not exist raises a KeyError and stops the program. Testing first turns that crash into an ordinary message.

>>> code = 'L-500'
>>> if code in issued:
        print("Issued to", issued[code])
    else:
        print("Book", code, "is not on issue")

Book L-500 is not on issue

Read the structure rather than the output. The lookup issued[code] appears only inside the branch that runs when the key is known to be present, so it can never be reached with a missing key. This pattern — test, then look up — is worth making a habit, and it is the same habit that Section 10.3 recommended before calling index() on a tuple.

Q. Would if issued[code]: work just as well as a test?

A. No. It looks up the key in order to test it, so on a missing key it raises KeyError before any test can happen. A guard has to ask whether the key exists without using it.

using membership to look for a value What students write:

>>> if 'Arun' in issued:
        print("Arun has a book")

What happens: nothing is displayed, even though Arun does hold a book. in tested the keys, found no key 'Arun', and the branch was skipped. The fix: membership on a dictionary is a question about keys. To ask about values you must go through the values themselves, which Section 10.11 provides a way to do.

Using the issued register above, write down your prediction for each of 'L-405' in issued, 'Kavya' not in issued and 'l-405' in issued. Run all three. For the third, say in one line why the answer is what it is.

10.10 Traversing a Dictionary

Looking up one value is what a dictionary is built for. But plenty of work needs every item — printing the whole register, adding up all the quantities, picking out the items that fall below a limit. Visiting every item of a collection in turn is called traversing it, and for a dictionary the for loop does the job.

There are two ways to write the loop, and the difference between them is what the loop variable receives on each turn.

Traversing by key

Write a for loop over the dictionary itself and the loop variable takes each key in turn. The value is then fetched with an ordinary lookup.

>>> sales = {'samosa': 42, 'idli': 18, 'poha': 27}
>>> for item in sales:
        print(item, sales[item])

samosa 42
idli 18
poha 27

Read the first line of that loop carefully, because it is the point students most often get wrong. The loop is written over sales, yet item holds 'samosa', not 42. Iterating a dictionary hands you its keys — never its values, and never the pairs. If the value is wanted, the loop body must ask for it, which is what sales[item] does.

Q. The loop above prints values as well. So in what sense does the loop only give keys?

A. The loop variable only ever holds a key. The value appears because the body performs a separate lookup with that key. Take out sales[item] and the loop prints keys alone.

Traversing by item

Often you want both parts of an item, and writing a lookup inside every loop body becomes tiresome. The items() method hands over each key-value pair together, and tuple assignment splits the pair into two names as the loop runs — exactly the unpacking of Section 10.4, performed once per turn.

>>> for item, quantity in sales.items():
        print(item, quantity)

samosa 42
idli 18
poha 27

Now item holds the key and quantity holds the value, and the body reads like the register it is printing. Fig. 10.10 shows what each of the two loops receives on a single turn.

Fig. 10.10

Fig. 10.10 – Dictionary traversal by key vs by item.

Table 10.16 – Traversing by key vs traversing by item

Feature By key By item
Loop written as for item in sales: for item, quantity in sales.items():
Loop variable holds One key One key and one value, unpacked into two names
Getting the value A lookup in the body, sales[item] Already in a named variable
Suits a body that uses the key mostly, or needs the value only sometimes uses both parts of every item
Number of loop variables One Two, matching the two parts of an item

Neither method is better in general. Where the body needs only the keys — deleting by a rule, listing item names — the first is simpler. Where it needs both, the second says more clearly what is going on.

unpacking without items() What students write:

>>> for item, quantity in sales:
        print(item, quantity)

What happens:

ValueError: too many values to unpack (expected 2)

The loop received the key 'samosa', a six-character string, and tried to split it into two names. The error is about the string, not the dictionary. The fix: add the method call — for item, quantity in sales.items():. Two loop variables need a source that hands over pairs.

The order of traversal

A traversal visits the items in the order in which they were inserted, so a dictionary built by reading a register from the top down prints back in that same order. This is a convenience of Python 3.7 onwards, as Section 10.7.1 noted, and it does not turn the items into a numbered sequence — there is still no way to ask for “the second item”. If a particular order is required, sort the keys yourself rather than depending on how the dictionary was built.

A traversal in practice

The problem below needs both kinds of loop, which makes it a fair test of the distinction.

reporting a day’s canteen sales and the slow-moving items Input: a dictionary in which each key is an item name and each value is the number of plates sold that day. Output: the full list of items with quantities, the total plates sold, and the names of items that sold fewer than twenty plates. Method:

1. Take the dictionary of sales and set a running total to 0.
2. Display a heading row.
3. For each key-value pair in the dictionary:
   3.1 Display the item name and its quantity.
   3.2 Add the quantity to the running total.
4. Display the running total.
5. For each key in the dictionary:
   5.1 If the value stored under that key is less than 20, display the key.

Why it works: step 3 visits every item exactly once, so every quantity is added exactly once and the running total is complete when the loop ends. Step 5 tests every item against the same limit, so no qualifying item can be missed. Watch out: the running total must be set to 0 before the loop and not inside it. Setting it inside would reset it on every turn, leaving only the last quantity at the end.

display a day’s canteen sales, the total sold and the slow-moving items Problem: from a dictionary of item names and quantities sold, display each item with its quantity, then the total plates sold, then the names of all items that sold fewer than twenty plates.

# To display a day's canteen sales, the total sold, and the slow-moving items

sales = {'samosa': 42, 'idli': 18, 'poha': 27, 'upma': 9}
total = 0

print("Item", "Sold", sep="\t")
for item, quantity in sales.items():
    print(item, quantity, sep="\t")
    total = total + quantity

print("Total plates sold:", total)

print("Sold fewer than 20:")
for item in sales:
    if sales[item] < 20:
        print(item)

Sample output:

Item    Sold
samosa  42
idli    18
poha    27
upma    9
Total plates sold: 96
Sold fewer than 20:
idli
upma

Identifier description:

Table 10.17 – Identifiers used in the canteen sales program

Identifier Type Purpose
sales Dictionary Maps each item name to the number of plates sold
total Integer Running total of plates sold, accumulated across the first loop
item String The item name being handled in the current turn of either loop
quantity Integer The quantity sold, received by unpacking in the first loop

Notes: the two loops are written differently on purpose. The first needs both parts of every item, so it unpacks; the second needs the key for display and the value only for a comparison, so a plain traversal with one lookup is enough. A dictionary is the right container here because sales are identified by item name, and because a name-to-quantity pair must never drift apart.

the first loop of the program, accumulating the total | Turn | item | quantity | Line displayed | total after this turn | |—|—|—|—|—| | Start | — | — | — | 0 | | 1 | ‘samosa’ | 42 | samosa 42 | 42 | | 2 | ‘idli’ | 18 | idli 18 | 60 | | 3 | ‘poha’ | 27 | poha 27 | 87 | | 4 | ‘upma’ | 9 | upma 9 | 96 |

Result: the loop ends with total holding 96, which the next line displays. The trace shows the running total carrying forward from one turn to the next — the reason it must be created before the loop begins.

Move the line total = 0 to just inside the first loop, as its first statement, and run the program again. Write down the total that is now printed and explain, in one line, why it is that particular number.

10.11 Dictionary Methods and Built-in Functions

Section 10.3 drew a line between a built-in function, called by name with the collection inside the brackets, and a method, which belongs to the object and is written after a full stop. The same line runs through this section. Dictionaries own a good many methods, and the built-in functions you already know work on them as well.

The dictionary below records how many periods a week each subject is taught.

>>> periods = {'Hindi': 3, 'English': 3, 'Maths': 4, 'Science': 5}
>>> len(periods)
4

len() counts items — that is, key-value pairs. Four subjects, four items. It never counts keys and values separately.

Building a dictionary from pairs

dict() with nothing inside the brackets gives an empty dictionary, as Section 10.7.1 showed. Given a sequence of pairs, it builds a dictionary from them, taking the first part of each pair as the key and the second as the value.

>>> pairs = [('Hindi', 3), ('English', 3)]
>>> dict(pairs)
{'Hindi': 3, 'English': 3}

This is how data that arrives as a list of records — from a file, or from another part of a program — is turned into something that can be looked up by key.

Getting the keys, the values and the items

Three methods hand back the contents of a dictionary in bulk. keys() gives its keys, values() gives its values, and items() gives the complete pairs — the method you already used for traversal in Section 10.10.

>>> periods.keys()
dict_keys(['Hindi', 'English', 'Maths', 'Science'])
>>> periods.values()
dict_values([3, 3, 4, 5])
>>> periods.items()
dict_items([('Hindi', 3), ('English', 3), ('Maths', 4), ('Science', 5)])

Look at how Python labels each result. What comes back is not a list but a view — an object that shows you the dictionary’s keys, values or items as they currently stand. A view can be looped over and tested with in, which covers nearly everything you will want to do with it. Where a genuine list is needed, convert it:

>>> list(periods.values())
[3, 3, 4, 5]

Notice that values() returned two 3s. Values may repeat, as Section 10.7.1 said, and the view reports every one of them. Keys, being unique, never repeat in a keys() view.

A view stays connected to the dictionary it came from. If an item is added after the view was made, the view shows the new item too — it is a window on the dictionary, not a copy of it. A list made with list() is a copy, and does not change afterwards.

Because a view can be looped over, the built-in functions of Section 10.3 apply to it directly. This is the neatest way to get totals and extremes out of a dictionary:

>>> load = {'Hindi': 3, 'English': 3, 'Maths': 4, 'Science': 5}
>>> sum(load.values())
15
>>> max(load.values())
5
>>> sorted(load.keys())
['English', 'Hindi', 'Maths', 'Science']

The last line is how you impose an order of your own on a dictionary. Sorting the keys gives a list you can loop over in alphabetical order, without disturbing the dictionary itself.

Looking up without the risk

get() looks up a key and returns its value, just as square brackets do — but on a missing key it does not raise KeyError. It returns None, Python’s way of saying “no value at all”.

>>> periods.get('Maths')
4
>>> periods.get('Music')
>>> print(periods.get('Music'))
None

The second line displayed nothing, because the shell does not echo None; printing it makes the result visible. A second argument may be given to say what should come back instead, which is often more useful than None:

>>> periods.get('Music', 0)
0

Now a subject not on the timetable reports zero periods, which is exactly right, and the calling program has no special case to handle.

Table 10.18 – Looking up with square brackets vs with get()

Feature periods['Music'] periods.get('Music')
When the key is present Returns the value Returns the value
When the key is absent Raises KeyError and stops the program Returns None, or the default you supply
Suits a situation where the key must be there, and its absence is a real fault the key may legitimately be missing
Needs a guard before it Yes, a membership test No
Can be used to assign Yes — periods['Music'] = 2 creates or updates No — it only reads

Neither form is the safer choice in every case. Square brackets are right when a missing key means something has gone wrong and you want to know at once; get() is right when absence is an ordinary possibility, as it is when counting things that may not have appeared yet.

Q5. What happens if you ask a dictionary for a key it does not have, and is there a way to ask that does not stop the program?

A. Using square brackets, Python raises a KeyError naming the missing key, and the program stops. There are two safe ways to ask. Test first with the membership operator and look up only if the key is present, or use the get() method, which returns None for a missing key — or any default value you choose to supply as a second argument.

Merging and emptying

update() takes another dictionary and folds its items into the one the method was called on. Keys not already present are added; keys already present have their values replaced.

>>> extra = {'Music': 2, 'Maths': 5}
>>> periods.update(extra)
>>> periods
{'Hindi': 3, 'English': 3, 'Maths': 5, 'Science': 5, 'Music': 2}

Both effects are visible there. 'Music' was new, so it joined at the end. 'Maths' already existed, so its 4 was replaced by 5 — the two values were not added together, and the item stayed where it was. The dictionary passed as the argument is left unchanged by all this:

>>> extra
{'Music': 2, 'Maths': 5}

clear() removes every item, leaving an empty dictionary. The dictionary itself survives; only its contents go.

>>> extra.clear()
>>> extra
{}

This is worth setting beside del from Section 10.8.3. extra.clear() leaves you with an empty dictionary you can go on using; del extra removes the name altogether, and using it afterwards raises NameError.

Table 10.19 – Methods and built-in functions for dictionaries, with what each one returns

Name Kind What it does Returns
len() Built-in function Counts the items — the key-value pairs An integer
dict() Built-in function Builds a dictionary, empty or from a sequence of pairs A dictionary
keys() Method Gives all the keys A view of the keys
values() Method Gives all the values, repeats included A view of the values
items() Method Gives all the key-value pairs A view of the items, each pair a tuple
get() Method Looks up a key without risking an error The value; None or a supplied default if the key is absent
update() Method Folds another dictionary’s items in, replacing existing keys Nothing; the dictionary is changed in place
clear() Method Removes every item Nothing; the dictionary is left empty
The methods at work

A stock register is the natural place to see three of these methods together — one to merge in a consignment, one to display the result, one to answer a query safely.

updating a stock register from a consignment and querying it Input: a dictionary of items already in stock, and a second dictionary of items received in a consignment. Output: the updated register displayed item by item, the number of item types held, and the quantity of one queried item. Method:

1. Take the stock dictionary and the consignment dictionary.
2. Fold the consignment into the stock, so that new items are added and
   quantities of existing items are replaced.
3. Display each item of the updated stock with its quantity.
4. Display the number of items the register now holds.
5. Look up the queried item safely, treating its absence as a quantity of zero,
   and display the result.

Why it works: step 2 leaves the register holding exactly one entry per item name, since keys are unique and a repeated key replaces rather than duplicates. Step 5 cannot fail, because the safe lookup supplies a value even for an item the register has never held. Watch out: step 2 replaces a quantity rather than adding to it. If a consignment is meant to increase an existing stock rather than restate it, the new quantity must be added to the old one item by item, and this method is the wrong tool.

update a stock register from a consignment and query one item Problem: fold a consignment dictionary into a stock dictionary, display the updated register, report how many item types it holds, and report the quantity of an item the register does not hold.

# To update a stock register with a consignment and check one item safely

stock = {'chalk': 30, 'duster': 12, 'marker': 25}
arrival = {'marker': 40, 'register': 15}

stock.update(arrival)

print("Updated stock")
for item, count in stock.items():
    print(item, count, sep="\t")

print("Number of item types:", len(stock))
print("Chart paper in stock:", stock.get('chart paper', 0))

Sample output:

Updated stock
chalk   30
duster  12
marker  40
register    15
Number of item types: 4
Chart paper in stock: 0

Identifier description:

Table 10.20 – Identifiers used in the stock update program

Identifier Type Purpose
stock Dictionary The register, mapping each item name to its quantity
arrival Dictionary The consignment received, in the same key-value form
item String The item name in the current turn of the display loop
count Integer The quantity in the current turn of the display loop

Notes: the marker count in the output is 40, not 65. update() restated the quantity rather than adding to it, which is exactly the limitation the algorithm warned about, and the reason the consignment here is written as a fresh count rather than as an increase. The final line uses get() with a default of 0 so that a query about an item never stocked reads sensibly instead of stopping the program.

Replace the update() call in the program with a loop that adds each consignment quantity to whatever the register already holds, using get() with a default of 0 for items the register does not yet have. Run it and check that the marker count now reads 65.

10.12 Manipulating Dictionaries

Every piece of the dictionary has now been introduced separately — creating, looking up, adding, changing, deleting, traversing and the methods. This section puts them to work on three problems of the kind that dictionaries were made for: building a table from data the user types in, counting how often things occur, and holding records that must be found by an identifier.

Building a dictionary from input

A dictionary built at the keyboard is built one item at a time, and the statement that does it is the ordinary assignment of Section 10.8.1. Since the key is read fresh each time, that statement adds a new item on every turn of the loop.

building a rate card from typed input and answering a price query Input: how many items are to be entered, then that many item names each followed by its price, then one item name to look up. Output: the completed rate card, and the price of the item asked for, or a message if it is not on the card. Method:

1. Read how many items are to be entered.
2. Start with an empty dictionary.
3. Repeat, as many times as the count says:
   3.1 Read an item name and its price.
   3.2 Store the price in the dictionary under the item name as key.
4. Display the completed dictionary.
5. Read the item name to look up.
6. Look it up safely, supplying a message as the substitute if it is absent, and display the result.

Why it works: step 3.2 uses the item name as the key, so each price is filed under the name that identifies it and can be found again without any search written by hand. Step 6 cannot fail, because the safe lookup returns the substitute instead of stopping the program. Watch out: if the same item name is typed twice, no second item is created — the later price silently replaces the earlier one, because keys are unique.

build a rate card from typed input and look up one price Problem: read a count and then that many item names with their prices, store them in a dictionary, display the completed rate card, and report the price of an item the user asks for.

# To build a rate card from user input and answer a price query

count = int(input("How many items? "))
rate = {}

for i in range(count):
    item = input("Enter item name: ")
    price = int(input("Enter price: "))
    rate[item] = price

print("Rate card:", rate)

wanted = input("Enter the item to look up: ")
print("Price:", rate.get(wanted, "not on the card"))

Sample output:

How many items? 3
Enter item name: notebook
Enter price: 45
Enter item name: pen
Enter price: 10
Enter item name: glue
Enter price: 30
Rate card: {'notebook': 45, 'pen': 10, 'glue': 30}
Price: 10

Identifier description:

Table 10.21 – Identifiers used in the rate card program

Identifier Type Purpose
count Integer How many items are to be entered
rate Dictionary Maps each item name to its price
i Integer Loop variable controlling how many items are read
item String The item name read in the current turn
price Integer The price read in the current turn
wanted String The item name the user asks about

Notes: compare this with the tuple built from input in Section 10.6. There the tuple had to be rebuilt on every turn by concatenation, because a tuple cannot grow; here the dictionary is simply added to, because it can. That is mutability doing real work rather than being a definition to remember.

Counting with a dictionary

Counting how often each thing occurs is the problem dictionaries handle better than any other structure, because the thing being counted can serve as the key. The count for each distinct word, letter, item code or grade lives under its own key, and no separate list of what has been seen so far is needed.

The difficulty is only at the start: the first time a word appears there is no count to add to. Two branches handle it — if the word is already a key, increase its value; if not, start it at 1.

counting how many times each word appears in a sentence Input: a line of text typed by the user, with words separated by spaces. Output: each distinct word, with the number of times it appears. Method:

1. Read a line of text and split it into a list of words.
2. Start with an empty dictionary.
3. For each word in the list:
   3.1 If the word is already a key in the dictionary, increase its value by 1.
   3.2 Otherwise, add the word as a new key with the value 1.
4. Traverse the dictionary and display each key with its value.

Why it works: after any number of words have been processed, the value stored under a key is exactly the number of times that word has been met so far, because step 3 adds one for every occurrence and none for anything else. When the list is exhausted, that count is the final one. Watch out: words are compared exactly as typed, so a capital letter or a stray full stop makes a different key. The and the are counted separately.

count how many times each word appears in a sentence Problem: read a sentence from the user and display each distinct word in it with the number of times it occurs.

# To count how many times each word appears in a sentence

line = input("Enter a sentence: ")
freq = {}

for word in line.split():
    if word in freq:
        freq[word] = freq[word] + 1
    else:
        freq[word] = 1

for word in freq:
    print(word, ":", freq[word])

Sample output:

Enter a sentence: the bus to the depot and the bus back
the : 3
bus : 2
to : 1
depot : 1
and : 1
back : 1

Identifier description:

Table 10.22 – Identifiers used in the word count program

Identifier Type Purpose
line String The sentence typed by the user
freq Dictionary Maps each distinct word to the number of times it occurs
word String The word being handled in the current turn of either loop

Notes: split() breaks the sentence at its spaces and hands back a list of words, which the first loop then walks through. The words are displayed in the order of their first appearance, because that is the order in which their keys were inserted.

the counting loop on the sentence “the bus to the depot” | Turn | word | Already a key? | Action taken | freq after this turn | |—|—|—|—|—| | Start | — | — | — | {} | | 1 | ‘the’ | no | Added with value 1 | {‘the’: 1} | | 2 | ‘bus’ | no | Added with value 1 | {‘the’: 1, ‘bus’: 1} | | 3 | ‘to’ | no | Added with value 1 | {‘the’: 1, ‘bus’: 1, ‘to’: 1} | | 4 | ‘the’ | yes | Value raised from 1 to 2 | {‘the’: 2, ‘bus’: 1, ‘to’: 1} | | 5 | ‘depot’ | no | Added with value 1 | {‘the’: 2, ‘bus’: 1, ‘to’: 1, ‘depot’: 1} |

Result: the dictionary ends holding four items, with 'the' counted twice. The trace shows that turn 4 changed a value without adding an item — the key 'the' was already there, so the dictionary’s length did not grow.

The two branches of that if can be collapsed into one line using get() from Section 10.11, with 0 as the substitute for a word not yet seen:

freq[word] = freq.get(word, 0) + 1

A word already counted has its value read and increased; a word met for the first time reads as 0 and becomes 1. Both cases are handled by the same statement, and the special case disappears.

Records held in a dictionary of tuples

The last problem draws the two halves of this chapter together. A student record has several fields that belong to one another and should not be altered piecemeal — a tuple. The records must be found by roll number rather than by position — a dictionary. Put one inside the other and you get a register that is both safe and quick to search, as Fig. 10.11 shows.

Fig. 10.11

Fig. 10.11 – Student records as a dictionary of tuples.

displaying a student record found by roll number Input: a dictionary in which each key is a roll number and each value is a tuple holding a name and a mark, and a roll number to display. Output: the record’s fields, displayed separately, or a message that no such record exists. Method:

1. Take the dictionary of records.
2. If the given roll number is a key in the dictionary:
   2.1 Look up its value, which is a tuple.
   2.2 Unpack that tuple into a name and a mark.
   2.3 Display the roll number, the name and the mark.
3. Otherwise, display that no record exists for that roll number.

Why it works: the roll number identifies the record uniquely, since keys cannot repeat, so at most one record can match. Unpacking gives each field its own name, so the display line states what it is showing instead of indexing into a tuple. Watch out: the unpacking in step 2.2 assumes every record has exactly two fields. A record stored with three fields would raise a ValueError at that step.

display a student record found by roll number Problem: from a dictionary mapping roll numbers to tuples of name and mark, display the record for a given roll number, or report that no such record exists.

# To store student records as a dictionary of tuples and display one record

records = {101: ('Ishaan', 74), 102: ('Farah', 88), 103: ('Tejas', 63)}

def show(roll):
    if roll in records:
        name, marks = records[roll]
        print("Roll:", roll, "Name:", name, "Marks:", marks)
    else:
        print("No record for roll", roll)

show(102)
show(110)

Sample output:

Roll: 102 Name: Farah Marks: 88
No record for roll 110

Identifier description:

Table 10.23 – Identifiers used in the student record program

Identifier Type Purpose
records Dictionary Maps each roll number to a tuple holding the student’s name and mark
roll Integer Parameter of the function; the roll number to be displayed
name String The name field, obtained by unpacking the record
marks Integer The mark field, obtained by unpacking the record

Notes: the keys here are integers, not strings, which is why the calls are written show(102) and not show('102') — a dictionary treats those as two different keys. The record itself is a tuple rather than a list so that no part of the program can change one field of a stored record; to correct a record, the whole tuple is replaced under the same key.

Q. Could the values in records have been lists instead of tuples?

A. Yes, and everything shown would still work. What you would lose is the guarantee: a list allows records[102][1] = 95, quietly rewriting a mark. The tuple refuses that, so any change to a record has to be a deliberate replacement.

Practical link: this task exercises dictionary creation, traversal and lookup from Sections 10.7 to 10.12, and is filed under Python programs in the practical report file. Write a program that reads the names of six subjects and the marks obtained in each into a dictionary, then displays the subject-wise marks, the total, the average, and the name of the subject with the highest mark. Test it once with all six marks equal, and say which subject your program reports as highest in that case and why.

10.13 Summary

  1. A tuple is an ordered sequence of elements written inside round brackets and separated by commas. Its elements may be of different types, and each element sits at a fixed index counted from 0.

  2. What makes a tuple is the comma, not the brackets. A single value inside brackets is just that value; a trailing comma is what turns it into a one-element tuple, and a bare comma-separated sequence is treated as a tuple even without brackets.

  3. Elements are reached by index, forwards from 0 or backwards from -1. An index outside the range the tuple actually has raises IndexError.

  4. A tuple is immutable: once created, its elements cannot be replaced, removed or added to. An assignment that appears to change a tuple in fact builds a new tuple and repoints the name to it.

  5. Immutability fixes which objects a tuple holds, not what those objects contain. A mutable element inside a tuple, such as a list, can still be changed from within.

  6. Four operations work on tuples. + concatenates two tuples into a new one; * repeats a tuple a whole number of times; in and not in test whether a value is one of the elements; and slicing extracts a run of elements as a new tuple.

  7. A slice runs from the start index up to but not including the stop index, with an optional step. An out-of-range slice is trimmed rather than refused, which is why slicing does not raise the error that indexing does.

  8. The functions and methods for tuples are len(), tuple(), count(), index(), sorted(), min(), max() and sum(). Of these, count() and index() are methods of the tuple; the rest are built-in functions. sorted() returns a new list, leaving the tuple unchanged.

  9. count() returns 0 for a value that is absent, but index() raises ValueError, so a membership test before it makes the lookup safe.

  10. Tuple assignment gives a tuple of values to a tuple of variables in one statement, matching them strictly by position. The counts on the two sides must be equal, or unpacking raises ValueError.

  11. Gathering values into a tuple is called packing and distributing them among several names is called unpacking. The whole right-hand side is evaluated and packed before any name on the left is assigned, which is what allows two variables to be exchanged in a single line and a function to return more than one value at once.

  12. A tuple whose elements are themselves tuples is a nested tuple. The outer index chooses a record and the inner index chooses a field within it, so a value is reached by two indices written one after the other.

  13. A dictionary is a collection of key-value pairs written inside curly braces, each pair joined by a colon. A key together with its value is called an item, and the value is reached through the key rather than through a position.

  14. Keys must be unique and of an immutable type — a number, a string or a tuple — because the storage location of an item is computed from its key. Values are under no such restriction and may repeat.

  15. Looking up an absent key with square brackets raises KeyError. A dictionary has no positions, so an integer written inside the brackets is treated as a key like any other.

  16. A dictionary is mutable and changes in place. Assigning to a key adds an item if the key is new and replaces the value if the key already exists — the same statement doing either, which is why a misspelt key silently creates an item instead of updating one.

  17. The del statement removes one item when given a key, and removes the whole dictionary when given only its name. The first leaves the dictionary shorter; the second leaves the name undefined.

  18. The membership operators in and not in test the keys of a dictionary and never its values. Testing before looking up is the standard way of avoiding KeyError.

  19. A dictionary is traversed with a for loop. Looping over the dictionary itself gives its keys one at a time; looping over items() gives each key and value together, to be unpacked into two names.

  20. The functions and methods for dictionaries are len(), dict(), sorted(), keys(), values(), items(), get(), update() and clear(). keys(), values() and items() return views, which can be traversed and tested but not indexed until converted to a list.

  21. get() returns the value for a key without raising an error when the key is absent, giving None or a substitute value supplied by the caller. update() merges another dictionary in, adding new keys and replacing existing ones. clear() empties a dictionary but leaves it in existence.

  22. Counting occurrences is the pattern a dictionary suits best: each distinct value becomes a key, a value met for the first time starts its count at 1, and every later occurrence adds 1 to the count already stored.

  23. The choice between the two types rests on how the data will be reached later. Data reached by position, and data that must not change, belongs in a tuple; data reached by a name or code, and data that will be added to and edited, belongs in a dictionary.

10.14 Key Terms

  1. Built-in function — a function provided by the Python language itself, called by name with the value it works on written inside the brackets, as in len(), sorted() or dict().

  2. Concatenation — joining two sequences end to end with the + operator, producing a new sequence that holds the elements of the first followed by those of the second.

  3. Dictionary — a collection of key-value pairs written inside curly braces, in which each value is reached through its key rather than through a position.

  4. Element — one of the values stored in a sequence such as a tuple.

  5. Immutable — describing an object whose value cannot be changed after it has been created; any operation that appears to change it builds a new object instead.

  6. Index — the position number of an element in a sequence, counted forwards from 0 or backwards from -1.

  7. Item — one key together with the value it maps to, forming a single entry of a dictionary.

  8. Key — the part of a dictionary item that identifies it; keys must be unique within a dictionary and must be of an immutable type.

  9. List — a mutable ordered sequence written inside square brackets, whose elements can be changed, added or removed after it is created.

  10. Mapping — a collection that stores pairs of a key and a value and retrieves a value directly from its key; the dictionary is Python’s mapping type.

  11. Membership operator — the operator in, which reports whether a value is an element of a sequence or a key of a dictionary; not in gives the opposite answer.

  12. Method — a function belonging to a particular object, called by writing the object’s name, a full stop and the method, as in count(), index(), get() or items().

  13. Mutable — describing an object whose contents can be changed in place after it has been created, as a list or a dictionary can.

  14. Nested tuple — a tuple in which one or more elements are themselves tuples, so that a value is reached by an outer index followed by an inner one.

  15. Packing — gathering several values into a single tuple, as happens on the right-hand side of a tuple assignment.

  16. Repetition — repeating a sequence a whole number of times with the * operator, producing a new sequence made of that many copies.

  17. Slicing — extracting a run of elements from a sequence by giving a start index, a stop index and optionally a step, the stop index itself being left out of the result.

  18. Traversing — visiting every element or item of a collection in turn, usually with a for loop.

  19. Tuple — an ordered, immutable sequence of elements written inside round brackets and separated by commas, whose elements are reached by index.

  20. Tuple assignment — assigning a tuple of values to a tuple of variables in one statement, matching them by position, with equal numbers on both sides.

  21. Unpacking — distributing the elements of a tuple among several variables, as happens on the left-hand side of a tuple assignment.

  22. Value — the part of a dictionary item that is identified by the key; values may be of any type and may repeat within a dictionary.

  23. View — the object returned by keys(), values() or items(), which reports on the dictionary as it currently stands and can be traversed or tested but not indexed.

10.15 Points to Ponder

  1. A tuple refuses to let you change it, and a list does not. Refusing to do something is usually a weakness in a tool. Why is it counted as a strength here — and can you describe a situation where using a list instead of a tuple would let a bug survive undetected for a long time?

  2. Python decided that the comma, and not the round bracket, is what makes a tuple. That decision is the reason (45) is a number while (45,) is a tuple. What does the language gain from this choice, and what would have to change elsewhere in Python if brackets alone had been made to do the job?

  3. A tuple can hold a list, and that list can be changed even though the tuple cannot. Is it fair, then, to call the tuple immutable? Try to state precisely what it is that immutability actually protects.

  4. Assigning to a key adds an item when the key is new and replaces a value when the key already exists, with no way to tell from the statement which will happen. Python could have provided two separate operations instead. What is gained by having one, and what is the cost of it?

  5. A dictionary can find a value from its key almost instantly, but finding which key holds a particular value means examining every item. Why should the two directions be so unequal, given that every item stores both parts?

  6. Keys must be immutable, and a tuple therefore qualifies as a key while a list does not. Suppose Python had allowed lists as keys. Describe, step by step, what would go wrong the moment such a key was modified after being stored.

10.16 Objective Questions

  1. Which of the following statements creates a tuple holding exactly one element? [1 mark]
    1. t = (7)
    2. t = (7,)
    3. t = [7]
    4. t = 7
  2. After the statement x = 4, 9, 16 is executed, the type of x is [1 mark]
    1. int
    2. list
    3. tuple
    4. dict
  3. A tuple has been created as t = (1, 2, 3). Which one of the following statements will raise an error? [1 mark]
    1. print(t[-1])
    2. print(t[1:5])
    3. t[0] = 5
    4. print(t * 2)
  4. Looking up a key that is not present in a dictionary using square brackets raises [1 mark]
    1. IndexError
    2. KeyError
    3. ValueError
    4. TypeError
  5. Assertion (A): A tuple may hold a list as one of its elements, and the contents of that list can be changed after the tuple has been created. Reason (R): A tuple is a mutable data type. [1 mark]
    1. Both A and R are true, and R is the correct explanation of A.
    2. Both A and R are true, but R is not the correct explanation of A.
    3. A is true but R is false.
    4. A is false but R is true.
  6. Assertion (A): The keys of a dictionary must be of an immutable type. Reason (R): The place where an item is stored is computed from its key, so a key that could change would no longer lead back to its own item. [1 mark]
    1. Both A and R are true, and R is the correct explanation of A.
    2. Both A and R are true, but R is not the correct explanation of A.
    3. A is true but R is false.
    4. A is false but R is true.
  7. Match each name in Column I with what it returns, from Column II. [1 mark]
Column I Column II
a. count() i. The value stored under a key, or None when the key is absent
b. index() ii. A view of the key-value pairs, each pair as a tuple
c. get() iii. The number of times a value occurs in a tuple
d. items() iv. The position of the first occurrence of a value in a tuple
  1. Fill in the blank: the ______ operator is used to test whether a given value is a key of a dictionary. [1 mark]

  2. State whether true or false: sorted() applied to a tuple returns a new tuple with the elements in ascending order. [1 mark]

  3. Write the output of the following statement. [1 mark]

print(('P',) * 3)

10.17 Check Your Concepts

  1. Define a nested tuple, and give one example of data that is naturally stored in one. [2 marks]

  2. The statement size = (12) does not create a tuple. Explain why, and write the corrected statement. [2 marks]

  3. count() and index() behave differently when the value being searched for is not present in the tuple. State the difference in two points. [2 marks]

  4. Differentiate between a tuple and a dictionary on any three points, giving one short example of each. [3 marks]

  5. A function in Python returns a single object. Explain, with a short example, how a function can nevertheless supply two results to its caller. [3 marks]

  6. A dictionary named stock holds item names as keys and quantities in hand as values. Write a single Python statement for each of the following. [4 marks]

    1. Add a new item 'marker' with quantity 18.
    2. Increase the quantity of 'pen' by 10.
    3. Remove the item 'eraser' from the dictionary.
    4. Display the number of items the dictionary currently holds.
  7. In a dictionary the keys must be immutable, while the values are under no such restriction. Explain the reason behind this rule, and state what would go wrong if a mutable key were allowed and were then modified. [4 marks]

  8. Write a function highest(readings) that accepts a tuple of numbers and returns both the largest number in it and the position at which that number first occurs, counting positions from 1. Explain in two or three lines why your method is correct, and state what your function should do if the tuple passed to it is empty. [5 marks]

10.18 Output & Debugging

  1. Write the output of the following code. [3 marks]
t = (5, 10, 15, 20, 25, 30)
print(t[1:5:2])
print(t[-2:])
print(len(t[2:2]))
  1. Write the output of the following code. [2 marks]
d = {'a': 1, 'b': 2}
d['c'] = 3
d['a'] = d['a'] + 5
print(len(d), d['a'])
  1. Write the output of the following code, and state in one line what the code computes in general. [3 marks]
data = ('red', 'blue', 'red', 'green', 'red')
count = {}
for c in data:
    count[c] = count.get(c, 0) + 1
print(count)
  1. The following code is intended to display each student’s name along with the marks. It stops with an error instead. Identify the error, name the line responsible, and rewrite that line correctly. [3 marks]
marks = {'Ravi': 61, 'Sita': 78}
for name, score in marks:
    print(name, score)
  1. The following code is meant to correct the middle reading and then display the total. Identify the error and state the message Python reports. Then rewrite the code so that the middle reading becomes 20 and the total is displayed, keeping the readings in a tuple. [3 marks]
readings = (12, 18, 24)
readings[1] = 20
print(sum(readings))

10.19 Case-Based Questions

  1. A school science laboratory keeps a record of its equipment. Each record holds an equipment code, the name of the item and the number of pieces available, and the whole record set is stored in the nested tuple below. [5 marks]
lab = (('EQ1', 'Beaker', 24),
       ('EQ2', 'Tripod stand', 8),
       ('EQ3', 'Test tube', 60),
       ('EQ4', 'Thermometer', 6))
a. Write a statement that displays the name of the item in the second record. [1 mark]
b. Write a loop that displays the code and name of every item of which fewer than 10 pieces are available. [2 marks]
c. The laboratory in-charge argues that this record set should have been kept in a list rather than a tuple, because stock changes as equipment breaks. Give one argument in favour of the tuple and one in favour of the list. [2 marks]
  1. A school library keeps the outstanding fine of each member in the dictionary below, with the member’s name as the key and the fine in rupees as the value. [5 marks]
fines = {'Rohit': 20, 'Naina': 0, 'Farhan': 35, 'Divya': 10}
a. Write a statement to record a new member `'Sameer'` with a fine of 25 rupees. [1 mark]
b. Write statements to increase Farhan's fine by 15 rupees and to remove Naina's entry from the register. [2 marks]
c. Write a loop that displays the name of every member whose fine is more than 12 rupees, along with the amount. [2 marks]

10.20 Programming Practice

  1. Write a complete program that accepts a count from the user, then reads that many whole numbers and stores them in a tuple. The program should display the tuple, the total of its elements, and how many of the elements are greater than the average. Submit the complete program. Sample input: a count of 5, followed by the values 40, 55, 30, 70 and 55. [3 marks]

  2. Write a function to_dictionary(records) that accepts a nested tuple in which every record holds an item code, an item name and a quantity, and returns a dictionary that maps each item code to its quantity. Submit the function together with an identifier description table. [4 marks]

  3. Write a complete program that accepts a line of text from the user and displays each distinct word in it along with the number of times that word occurs, with the words shown in alphabetical order. Words are separated by spaces. Submit the complete program. Sample input: the bus left the stop and the bus returned [4 marks]

  4. A small shop keeps its rate list in a dictionary, with the item name as the key and the price per unit as the value. Write a complete program that sets up such a rate list for at least four items, then repeatedly accepts an item name and a quantity from the customer until the user enters DONE, and finally displays the total amount payable. An item name that is not in the rate list must be reported without stopping the program. Submit the complete program, an identifier description table, and one sample run showing an unknown item being entered. [5 marks]