Chapter 9: Lists

In 1958, at the Massachusetts Institute of Technology, John McCarthy began work on a programming language for problems that had no fixed size — symbolic reasoning, algebra, the kind of task where you do not know in advance how many things you will end up holding. Every language of the time made you declare that number in advance. McCarthy’s answer was to build the entire language around a single structure: an ordered collection of items that could grow, shrink and be rearranged while the program ran. He called the language LISP, from list processing, and the structure it processed became one of the oldest surviving ideas in computing.

More than thirty years later, when Guido van Rossum designed Python, the same idea appeared again — this time as an everyday data type any beginner could use on the first day. You have already met one ordered sequence in Python: the string. It is ordered and it can be sliced, but it holds only characters, and once created it cannot be altered. Real data is rarely so obliging. A student record carries a name, a roll number, five marks and a percentage — different types, and marks that may need correcting after the paper is rechecked.

Fig. 9.0

Fig. 9.0 – Comparing an immutable string and a mutable list.

NoteCuriosity Corner

Q1. A string can hold only characters. How can one list hold a number, a piece of text and another whole list at the same time? Q2. If you write one list name equal to another, why can changing one of them change the other as well? Q3. What is the difference between adding a list to a list and adding each of its elements to a list? Q4. When a list is handed over to a function, can the function change it permanently? Q5. Asking a list for an element beyond its last position gives an error, but slicing it beyond its last position does not. Why?

This chapter will answer each of these questions.

9.1 Introduction to List

Suppose the class teacher asks you to write a program that reads the marks of forty students and prints the highest. With what you already know, you would need forty variables — m1, m2, m3, and so on down to m40 — and forty separate comparisons. Change the class strength to forty-five and the whole program has to be rewritten.

The difficulty is not the arithmetic. It is that the data is naturally one thing — a set of marks belonging together — while the program is forced to treat it as forty unrelated things. What is needed is a way to hold many values under a single name, and to reach any one of them by saying which position it occupies.

Python provides exactly this in the list. A list holds any number of values in a definite order, under one name. Each value stored in it is called an element, and the elements are written inside square brackets, separated by commas.

>>> marks = [72, 65, 88, 91, 54]
>>> print(marks)
[72, 65, 88, 91, 54]

One name, marks, now refers to all five values. The class strength no longer decides how many variables you write.

Ordered, but not sorted

A list is a sequence, which means its elements have positions and those positions are fixed until you change them. The word ordered here is a common source of confusion: it does not mean sorted. The list above is ordered because 72 is first and 54 is last, and it will stay that way; it is certainly not in increasing order. Order in this sense means only that every element has a place, and that the place is remembered.

NoteDefinition

A list is an ordered sequence of elements, enclosed in square brackets and separated by commas, in which the elements may be of different data types and may be changed after the list has been created.

You have already worked with one ordered sequence — the string. A string is ordered and can be indexed and sliced, but every element in it is a character, and once a string is created it cannot be altered. Real data is rarely so uniform, which is where the list earns its place.

9.1.1 Creating a List

A list is created by writing its elements inside square brackets. The values may be typed in directly, or they may be the results of expressions.

creating a list

list_name = [element1, element2, element3]

Parts: list_name is any valid Python identifier; each element is a value or an expression that produces a value; the square brackets are compulsory and the commas separate one element from the next. Returns / Effect: a new list object is created in memory and the name is made to refer to it. Example:

>>> fares = [12, 18, 25, 30, 44, 50]

The elements need not be of one type. A single list can hold an integer, a floating-point number, a string and a Boolean value together, because a list stores whatever it is given without insisting that all the items match.

>>> record = ['Notebook', 45, 12.5, True]
>>> print(record)
['Notebook', 45, 12.5, True]

This is what makes a list suitable for a record — a stationery item with its name, quantity in stock, unit price, and whether it is currently available. A string could hold the name; it could not hold the price as a number.

Q1. A string can hold only characters. How can one list hold a number, a piece of text and another whole list at the same time?

A. A string is defined as a sequence of characters, so a character is the only kind of element it can contain. A list is not defined by the type of its elements at all — it is defined only by the fact that it keeps elements in order. Each position in a list simply refers to some object, and Python does not require those objects to be alike. So one position may refer to an integer, the next to a string, and the next to another list.

Lists that hold nothing, or one thing

A list with no elements at all is written as a pair of empty square brackets, and it is perfectly legal. Programs often begin with an empty list and fill it as data arrives.

>>> waiting = []
>>> print(waiting)
[]
>>> len(waiting)
0

A list with a single element still needs its brackets, and it is not the same thing as the element itself. The brackets are what make it a list, and type() will confirm this.

>>> single = [99]
>>> type(single)
<class 'list'>
>>> type(99)
<class 'int'>

9.1.2 Accessing Elements in a List

Holding five values under one name is only half of what is needed. You must also be able to reach one particular value. This is done exactly as it is done with a string: by writing the position, called the index, inside square brackets after the list name.

Indices begin at 0, not at 1. The first element of a list of six fares sits at index 0 and the last at index 5, as shown in Fig. 9.1. Python also allows counting backwards from the right using negative indices, where \(-1\) is the last element and \(-6\) the first.

Fig. 9.1

Fig. 9.1 – Positive and negative indices of a list.

NoteDefinition

An index is the position of an element in a list, counted forward from 0 for the first element, or counted backward from −1 for the last element.

>>> fares = [12, 18, 25, 30, 44, 50]
>>> fares[0]
12
>>> fares[4]
44
>>> fares[-1]
50
>>> fares[-6]
12

The index need not be written as a plain number. Anything that evaluates to an integer will do, which is what allows a list to be used inside a loop.

>>> fares[2 + 1]
30

The built-in function len() returns the number of elements in a list. Since counting starts at 0, the last valid index is always one less than the length, and this is a relationship worth fixing in your mind early.

>>> len(fares)
6
>>> fares[len(fares) - 1]
50

Q. A list has six elements. Why is fares[6] an error when the list clearly has a sixth element?

A. It does have a sixth element, but that element is at index 5. Because numbering starts at 0, the six positions are 0 to 5, and index 6 names a seventh position that does not exist.

using the length as an index What students write:

>>> fares = [12, 18, 25, 30, 44, 50]
>>> fares[len(fares)]

What happens: Python stops with IndexError: list index out of range. The list has six elements, so len(fares) is 6, but the highest index in use is 5. The fix: write fares[len(fares) - 1], or simply fares[-1], both of which name the last element.

An index that is too large in the negative direction fails in the same way. For a list of six elements, fares[-7] raises IndexError as well, because there is no seventh position to count back to.

9.1.3 Lists are Mutable

A string, once created, cannot be altered. If you try to replace one of its characters, Python refuses.

>>> name = 'Meera'
>>> name[0] = 'V'
TypeError: 'str' object does not support item assignment

A list behaves quite differently. Writing an assignment with an index on the left replaces the element at that position, and the list itself carries on existing with its new contents. This property is called mutability, and a list is said to be mutable; a string, which does not allow it, is immutable.

>>> stock = ['pen', 'pencil', 'eraser', 'ruler']
>>> stock[2] = 'sharpener'
>>> stock
['pen', 'pencil', 'sharpener', 'ruler']
NoteDefinition

An object is mutable if its contents can be changed after it has been created, without creating a new object. A list is mutable; a string is immutable.

The phrase without creating a new object is the whole point, and Fig. 9.2 makes it visible. When stock[2] is assigned, no second list appears. The same list is edited where it stands, and the name stock goes on referring to it. This is why a list is the natural structure for data that gets corrected, extended or reordered while a program runs — a stock register, a marks sheet, a queue of pending tasks.

Fig. 9.2

Fig. 9.2 – Modifying a list element in place.

Q. Does stock[2] = 'sharpener' create a new list?

A. No. It changes the existing list in place. The name stock continues to refer to the same list object, which now holds a different third element.

The difference between a string and a list is worth setting out plainly, because almost every mistake in this chapter comes from expecting one to behave like the other.

Table 9.1 – String vs List

Feature String List
Written as Characters inside quotes, 'Meera' Elements inside square brackets, [72, 65, 88]
Type of elements Characters only Any type, and the types may differ within one list
Order of elements Ordered, indices from 0 Ordered, indices from 0
Changing an element Not allowed; raises TypeError Allowed; the element is replaced in place
Effect of a change A new string must be built instead The same object is altered, no new object is made
Suitable for Text that stays fixed once formed Data that is corrected, extended or reordered
Correcting a value in place

A marks sheet is the everyday example of data that must be editable. When a paper is re-evaluated, one mark changes and the rest stay untouched — precisely the operation a list supports.

replacing one mark in a list of marks after re-evaluation Problem: A list holds the marks of five students in the order of their roll numbers. The program must accept the position of the mark to be corrected and the corrected value, replace that one mark, and display the list before and after the change.

# Replaces one mark in a marks list after re-evaluation

marks = [72, 65, 88, 91, 54]
print("Marks before correction:", marks)

position = int(input("Enter the position to correct (0 to 4): "))
new_mark = int(input("Enter the corrected mark: "))

old_mark = marks[position]
marks[position] = new_mark

print("The mark", old_mark, "has been replaced by", new_mark)
print("Marks after correction:", marks)

Sample output:

Marks before correction: [72, 65, 88, 91, 54]
Enter the position to correct (0 to 4): 3
Enter the corrected mark: 95
The mark 91 has been replaced by 95
Marks after correction: [72, 65, 88, 95, 54]

Identifier description:

Table 9.2 – identifiers used in the mark-correction program, with their types and purposes

Identifier Type Purpose
marks list Holds the five marks in roll number order
position int Index of the mark that has to be corrected
new_mark int The corrected mark supplied by the user
old_mark int The mark that was there before, kept so it can be reported

Notes: old_mark is read out before the assignment, because after the assignment the earlier value is gone — nothing anywhere holds it. The program trusts the user to type a position between 0 and 4; a position outside that range would raise IndexError, and guarding against that is a matter taken up when conditional checks are added to such programs.

Create a list of the ticket fares for five bus stops on a route. Print the fare for the third stop using a forward index and then print the same fare using a negative index, so that both statements give the same value. Now raise the fare for the last stop by 5 without retyping the whole list, and print the list again.

9.2 List Operations

Section 9.1 dealt with one element at a time — reading it, replacing it. Python also lets you work with a list as a whole: join two lists, repeat one, ask whether a value is present in it, or pull out a part of it. Four operations do this work, and they are written with operators you have already used on strings, which is a deliberate design decision: a sequence is a sequence, and Python tries to make sequences behave alike.

Three of these four produce a new list and leave the original untouched. Keeping that in mind will save a great deal of confusion later, because it is the opposite of what index assignment did in Section 9.1.3.

9.2.1 Concatenation

Joining two lists end to end is called concatenation, and it is written with the + operator. The elements of the left list come first, followed by the elements of the right list, in their original order.

>>> morning = [12, 18, 25]
>>> evening = [30, 44]
>>> morning + evening
[12, 18, 25, 30, 44]

The result is a third list. Neither morning nor evening has changed, as Fig. 9.3 shows, and if you want to keep the joined list you must assign it to a name.

Fig. 9.3

Fig. 9.3 – Concatenating two lists.

>>> morning
[12, 18, 25]
>>> full_day = morning + evening
>>> full_day
[12, 18, 25, 30, 44]

Both operands of + must be lists. Python will not quietly convert something else for you, because it has no way of knowing whether you meant to add one element or to spread a value out across several.

concatenating a list with something that is not a list What students write:

>>> morning = [12, 18, 25]
>>> morning + 50

What happens: Python stops with TypeError: can only concatenate list (not "int") to list. The same error appears, with "str" in place of "int", if a string is used on the right. The fix: put the single value in a list of its own — morning + [50] — so that both operands are lists.

9.2.2 Repetition

The * operator repeats a list a given number of times, producing a new list. This operation is called repetition, and one operand must be a list while the other must be an integer.

>>> ['-'] * 5
['-', '-', '-', '-', '-']

The obvious use is drawing separator lines, but the useful one is setting up a list of counters that all start at the same value. If six buses run on a route and you want to count the passengers on each, a list of six zeros is exactly the starting point you need.

>>> passengers = [0] * 6
>>> passengers
[0, 0, 0, 0, 0, 0]

Without repetition you would have to type the six zeros yourself, which is tedious for six and impossible when the number is decided while the program runs. Written as [0] * n, the same statement builds a list of whatever size n happens to hold.

Repeating a list zero times is not an error — it gives an empty list. So [0] * 0 produces [], and so does any negative count. This is convenient in a program that computes the size from data, because a count of zero simply yields nothing rather than stopping the program.

9.2.3 Membership

To find out whether a value is present in a list, Python offers the membership operator in. It returns True if the value occurs as an element of the list and False otherwise. Its partner not in returns the opposite.

>>> stops = ['Ajmeri Gate', 'Sindhi Camp', 'Chandpole']
>>> 'Sindhi Camp' in stops
True
>>> 'Amer' in stops
False
>>> 'Amer' not in stops
True

This is a great deal shorter than writing a loop that compares each element in turn, and it is the natural way to ask a question such as is this book code in the catalogue? before doing anything with it.

There is one trap. When in is applied to a string it looks for a part of the string, but when it is applied to a list it looks for a whole element. A part of an element will not do.

Table 9.3 – the in operator on a string and on a list

Feature On a string On a list
What is searched for Any run of consecutive characters One complete element
'Sindhi' in 'Sindhi Camp' True, because it is a part of the text Not applicable
'Sindhi' in ['Sindhi Camp'] Not applicable False, because the only element is 'Sindhi Camp', not 'Sindhi'
Matching rule Character by character, case-sensitive Element by element, and the whole element must be equal
Value returned True or False True or False

searching for a part of an element What students write:

>>> stops = ['Ajmeri Gate', 'Sindhi Camp', 'Chandpole']
>>> 'Sindhi' in stops

What happens: the result is False, with no error message at all. The program runs on and takes the wrong branch, which makes this harder to notice than a crash would be. The fix: compare the whole element — 'Sindhi Camp' in stops — or, if a partial match really is wanted, test the elements one at a time using a loop, which Section 9.3 introduces.

9.2.4 Slicing

Sometimes only a part of a list is wanted: the first three fares, the last two stops, every alternate reading. Extracting a part of a list is called slicing, and the part extracted is called a slice. As with strings, two indices separated by a colon give the range.

slicing a list

list_name[start:end:step]

Parts: start is the index where the slice begins and is included; end is the index where the slice stops and is not included; step is how far to move each time and may be left out, in which case it is 1. Any of the three may be omitted. Returns / Effect: a new list containing the selected elements. The original list is not changed. Example:

>>> fares = [12, 18, 25, 30, 44, 50]
>>> fares[1:4]
[18, 25, 30]

The end index being excluded is the one rule students most often get wrong, so it is worth picturing the indices not as labels on the boxes but as the cuts between them, as drawn in Fig. 9.4. A slice takes everything between the cut at start and the cut at end.

Fig. 9.4

Fig. 9.4 – Slicing a list between index cuts.

A count follows from this picture: the number of elements in list_name[start:end] is \(end - start\), provided both indices lie within the list. The slice fares[1:4] therefore holds \(4 - 1 = 3\) elements.

Leaving out the indices

If start is omitted, the slice begins at the first element. If end is omitted, it runs to the last element. Omitting both gives a slice containing everything — a point Section 9.6 will put to serious use.

>>> fares[:3]
[12, 18, 25]
>>> fares[3:]
[30, 44, 50]
>>> fares[:]
[12, 18, 25, 30, 44, 50]

Negative indices may be used inside a slice as well, and they mean what they meant in Section 9.1.2 — positions counted from the right.

>>> fares[-4:-1]
[25, 30, 44]
Indices that do not fit

Indexing and slicing part company when an index is too large. A single index beyond the end of the list is an error; a slice beyond the end of the list is not.

>>> fares[4:20]
[44, 50]
>>> fares[5:2]
[]

Q5. Asking a list for an element beyond its last position gives an error, but slicing it beyond its last position does not. Why?

A. The two operations ask different questions. fares[6] demands one specific element, and if there is no element at position 6 there is nothing that could be returned, so Python raises IndexError. A slice asks instead for whatever lies between two boundaries. If the far boundary lies past the end of the list, the answer is simply everything up to the end — a shorter slice, but still a perfectly good one. If the boundaries enclose nothing at all, as in fares[5:2], the honest answer is the empty list.

Slicing with a step

A third number after a second colon says how far to move each time. A step of 2 picks alternate elements, and a step of \(-1\) walks the list from right to left, which is the shortest way in Python to obtain a reversed copy.

>>> fares[0:6:2]
[12, 25, 44]
>>> fares[::2]
[12, 25, 44]
>>> fares[::-1]
[50, 44, 30, 25, 18, 12]

Q. After all these slicing statements, what does fares itself hold?

A. The original six fares, unchanged. Slicing reads a list and builds a new one from what it read; it never disturbs the list it was applied to.

Putting the operations together

The four operations are rarely used alone. A short program that maintains a library catalogue uses three of them in a row — concatenation to combine two shelves, membership to answer a query, and slicing to display only the opening entries.

combining two shelf lists and searching the catalogue Problem: Two lists hold the book codes kept on two shelves. The program must build one catalogue from both shelves, report how many books it holds, tell the user whether a code they enter is present, and finally display the first three codes of the catalogue.

# Combines two shelf lists into one catalogue and searches it

shelf_a = ['B101', 'B102', 'B103']
shelf_b = ['B204', 'B205']

catalogue = shelf_a + shelf_b
print("Catalogue:", catalogue)
print("Number of books:", len(catalogue))

code = input("Enter the book code to search: ")
if code in catalogue:
    print(code, "is available in the library")
else:
    print(code, "is not in the catalogue")

print("First three codes:", catalogue[:3])

Sample output:

Catalogue: ['B101', 'B102', 'B103', 'B204', 'B205']
Number of books: 5
Enter the book code to search: B205
B205 is available in the library
First three codes: ['B101', 'B102', 'B103']

Identifier description:

Table 9.4 – identifiers used in the catalogue program, with their types and purposes

Identifier Type Purpose
shelf_a list Book codes kept on the first shelf
shelf_b list Book codes kept on the second shelf
catalogue list The combined list built by concatenation
code str The book code entered by the user for searching

Notes: The result of shelf_a + shelf_b is assigned to catalogue, because writing the expression alone would build the joined list and then discard it. The comparison is case-sensitive, so b205 typed in lower case would not be found — a reminder that membership on a list demands an exact match.

Take a list of the six fares used in this section. Without typing the numbers again, write single statements that give (a) the last three fares, (b) every alternate fare starting from the second one, and (c) the whole list in reverse order. Then print the original list once more and confirm that none of the three statements has altered it.

9.3 Traversing a List

Reaching one element by its index answers a question about one element. Most real questions are about all of them. How many days did the library issue more than twenty books? What is the highest reading of the week? What is the total fare along the route? None of these can be answered without visiting every element in turn.

Visiting each element of a list, one after another, is called traversal. Each visit is one iteration of the loop that performs it, and the loop is what turns a list from a store of values into something a program can reason about. Fig. 9.5 shows the idea: a marker moves along the list from the first element to the last, and at each stop the program does whatever the task requires.

Fig. 9.5

Fig. 9.5 – Traversing a list element by element.

NoteDefinition

Traversal is the process of visiting every element of a list exactly once, in order, so that some operation can be carried out on each of them.

Python offers two loops for the job — for and while — and the choice between them is not a matter of taste. Each fits a different shape of problem.

9.3.1 Traversal Using a for Loop

The simplest traversal asks Python to hand you the elements themselves. The for loop takes the list, gives its first element to a variable, runs the body, then gives it the second element, and so on until the list is exhausted.

>>> stops = ['Ajmeri Gate', 'Sindhi Camp', 'Chandpole']
>>> for stop in stops:
        print(stop)

Ajmeri Gate
Sindhi Camp
Chandpole

Here stop is the loop variable. On the first iteration it refers to the first element, on the second to the second, and so on. Nothing in the loop mentions an index at all, which is exactly why this form is preferred whenever the position of an element does not matter — the shortest correct loop is also the hardest one to get wrong.

Q. After the loop has finished, does the loop variable still exist?

A. Yes. It holds the last element the loop assigned to it, 'Chandpole' in this case. It is an ordinary variable, not something the loop creates and destroys, so it is still usable — though relying on its value after the loop makes a program hard to read.

Traversal when the position matters

Some tasks need the index as well as the element: reporting which day had the highest issue, or comparing an element with the one before it. For these, the loop is run over the indices instead, using range() together with len().

The expression range(len(books)) produces the whole set of valid indices, from 0 up to one less than the length — precisely the positions that exist in the list. The element at each position is then reached with books[i].

>>> books = [14, 9, 22, 17, 22, 6]
>>> for i in range(len(books)):
        print(i, books[i])

0 14
1 9
2 22
3 17
4 22
5 6

Notice that len() is called on the list, not typed as a fixed number. Write range(6) and the loop breaks the moment the list grows or shrinks; write range(len(books)) and it adjusts itself.

Scanning for the largest value

A classic traversal problem is finding the largest element. There is a built-in function that does this, and Section 9.4 will introduce it, but the method behind it is worth building yourself, because the same pattern — remember the best seen so far, and improve it — solves a great many problems that no built-in function covers.

finding the largest element of a list and its position Input: a list of numbers with at least one element. Output: the largest value in the list, and the index at which it occurs. Method:

1. Take the first element as the best value so far, and 0 as its position.
2. Repeat steps 3 to 4 for every index i from 1 up to the last index.
3.    If the element at index i is greater than the best value so far, then
4.       make it the new best value, and record i as its position.
5. Report the best value and its recorded position.

Why it works: every element after the first is compared once with the best value found up to that point. A value is replaced only by something larger, so when the last comparison is over, the value held is not smaller than any element of the list — which is what “largest” means. Watch out: the method assumes the list is not empty; on an empty list, step 1 has no first element to take and the program stops with IndexError. If the largest value occurs more than once, the strict comparison in step 3 keeps the earliest position, because a later equal value is not greater.

finding the busiest day in a week’s library issue record Problem: A list holds the number of books issued by a school library on each of the six working days of a week, in order. The program must find the largest of these numbers and the position in the list at which it occurs, and display both.

# Finds the largest value in a list of daily issue counts, and its position

books = [14, 9, 22, 17, 22, 6]

best = books[0]
best_pos = 0

for i in range(1, len(books)):
    if books[i] > best:
        best = books[i]
        best_pos = i

print("Books issued during the week:", books)
print("Highest number issued on a day:", best)
print("Position of that day in the list:", best_pos)

Sample output:

Books issued during the week: [14, 9, 22, 17, 22, 6]
Highest number issued on a day: 22
Position of that day in the list: 2

Identifier description:

Table 9.5 – identifiers used in the busiest-day program, with their types and purposes

Identifier Type Purpose
books list Number of books issued on each working day, in day order
best int Largest number of books seen so far during the traversal
best_pos int Index at which best was found
i int Loop variable holding the index currently being examined

Notes: The loop begins at index 1, not 0, because index 0 has already been used to set the starting value; comparing the first element with itself would waste an iteration without changing anything. The traversal makes exactly one pass over the list, so a week of six days and a term of a hundred days cost the same per element.

The trace below follows the two remembered values through the whole loop. Watch what happens at index 4, where the second 22 appears.

the busiest-day program on the list [14, 9, 22, 17, 22, 6] | Iteration | i | books[i] | Is books[i] > best? | best after | best_pos after | |—|—|—|—|—|—| | before the loop | — | — | — | 14 | 0 | | 1 | 1 | 9 | No | 14 | 0 | | 2 | 2 | 22 | Yes | 22 | 2 | | 3 | 3 | 17 | No | 22 | 2 | | 4 | 4 | 22 | No | 22 | 2 | | 5 | 5 | 6 | No | 22 | 2 |

Result: the program prints 22 and position 2. The trace shows why the answer is position 2 and not position 4: at index 4 the test asks whether 22 is greater than 22, which it is not, so the earlier position survives. Replacing > with >= would report the later day instead — a one-character change with a visible effect on the answer.

9.3.2 Traversal Using a while Loop

The same traversal can be written with a while loop, but nothing is handed to you automatically. You create the index yourself, test it yourself, and increase it yourself.

>>> books = [14, 9, 22, 17, 22, 6]
>>> i = 0
>>> while i < len(books):
        print(books[i])
        i = i + 1

14
9
22
17
22
6

Three statements do the work of the for header: i = 0 starts the count, i < len(books) decides whether another element remains, and i = i + 1 moves to the next one. Fig. 9.6 shows the cycle they form.

Fig. 9.6

Fig. 9.6 – Execution flow of a while loop traversing a list.

leaving out the counter update What students write:

i = 0
while i < len(books):
    print(books[i])

What happens: i stays 0 for ever, so the condition never becomes false and the first element is printed endlessly. The program has to be stopped by hand. The fix: put i = i + 1 inside the loop, as the last statement of the body. In a for loop this step is built into the loop itself, which is one reason it is the safer choice for a plain traversal.

When a while loop is the better tool

If a while loop is more work, why use it at all? Because a for loop over a list runs to the end, while a while loop can stop the moment its condition fails. When the question is where does something first happen, there is no reason to examine the rest of the list.

>>> books = [14, 9, 22, 17, 22, 6]
>>> i = 0
>>> while i < len(books) and books[i] <= 20:
        i = i + 1

>>> i
2

The condition has two parts. The first keeps the index inside the list; the second keeps the loop going only while the readings are small. The loop stops at index 2, the first day on which more than twenty books were issued, without ever looking at the last three days. Note the order of the two tests — the index check must come first, because if it did not, the loop would try to read books[i] at a position that does not exist once the list ran out.

Table 9.6 – three ways of traversing a list

Feature for over elements for over range(len(...)) while with a counter
What the loop variable holds The element itself The index of the element The index, maintained by you
Index available in the body No Yes Yes
Who advances the loop Python Python You, with an update statement
Can it stop before the end Not on its own Not on its own Yes, through its condition
Risk of an endless loop None None Real, if the counter is not updated
Best suited to Doing the same thing to every element Tasks needing the position, or comparing neighbours Searching for the first element that satisfies a condition

Take a list of the marks scored by six students. Write a traversal that prints only the marks above 50, then a second traversal that prints the position of every such mark as well. Now rewrite the first one as a while loop, run it, and then delete the counter update and predict — before running it — what you will see on the screen.

9.4 List Methods and Built-in Functions

So far, everything done to a list has been done with operators, indices and loops. Python also supplies a set of ready-made operations for the jobs that come up again and again — adding an element, removing one, counting occurrences, sorting. These arrive in two forms, and telling them apart matters, because they are written differently and they behave differently.

A built-in function such as len() is written with the list inside its brackets: len(books). A method such as append() is written after the list, joined to it by a dot: books.append(30). The dot is not decoration. It says that the operation belongs to the list and is being asked to work on that particular list.

NoteDefinition

A method is a function that belongs to an object and is called by writing the object, a dot, and the method name with its arguments in brackets. A list method acts on the list it is called on.

The second difference is subtler and causes more trouble. Most list methods change the list in place — they alter the existing list and hand back nothing at all. Built-in functions such as len() and sum() do the opposite: they leave the list alone and hand back a value. Keep asking yourself which of the two a given operation is, and half the errors in this section disappear.

9.4.1 Built-in Functions Used with Lists

Four of the built-in functions you already know work directly on a list of numbers, and one more builds a list out of some other sequence. All of them read the list and return a value; none of them changes it.

>>> readings = [31, 27, 35, 29]
>>> len(readings)
4
>>> min(readings)
27
>>> max(readings)
35
>>> sum(readings)
122

Together these answer a surprising number of questions in one line each. The average of the readings, for instance, needs no loop at all.

>>> sum(readings) / len(readings)
30.5

The function list() goes the other way: given a sequence, it produces a list of that sequence’s elements. Given nothing, it produces an empty list.

>>> list('aeiou')
['a', 'e', 'i', 'o', 'u']
>>> list(range(1, 6))
[1, 2, 3, 4, 5]
>>> list()
[]

The second of these is worth remembering. range() by itself does not produce a list you can index or slice; wrapping it in list() does.

Table 9.7 – built-in functions commonly used with lists

Function What it returns Effect on the list
len() The number of elements in the list None; the list is only read
min() The smallest element None
max() The largest element None
sum() The total of all elements None
list() A new list built from the sequence passed to it Builds a new list

using a numeric function on a list of text What students write:

>>> names = ['Meera', 'Arjun']
>>> sum(names)

What happens: Python stops with TypeError: unsupported operand type(s) for +: 'int' and 'str'. sum() begins with a total of 0 and tries to add each element to it, and a name cannot be added to a number. The fix: use sum() only on lists of numbers. len() works on any list, whatever it holds, because counting elements does not require adding them.

9.4.2 Adding Elements to a List

Three methods put new elements into a list. They differ in where the element goes and in how many elements arrive.

The method append() takes exactly one element and puts it at the end.

>>> stock = ['pen', 'pencil']
>>> stock.append('eraser')
>>> stock
['pen', 'pencil', 'eraser']

The word one is exact. If the argument handed to append() happens to be a list, that entire list becomes a single element of the outer list — one box holding four items rather than four boxes.

>>> stock.append(['ruler', 'sharpener'])
>>> stock
['pen', 'pencil', 'eraser', ['ruler', 'sharpener']]
>>> len(stock)
4

When the intention is to add the items separately, the method needed is extend(). It expects a list, opens it up, and adds each of its elements to the end of the original.

>>> stock = ['pen', 'pencil']
>>> stock.extend(['ruler', 'sharpener'])
>>> stock
['pen', 'pencil', 'ruler', 'sharpener']
>>> len(stock)
4

The contrast is drawn in Fig. 9.7, and it is the single most common confusion in list handling.

Fig. 9.7

Fig. 9.7 – Difference between append() and extend().

Q3. What is the difference between adding a list to a list and adding each of its elements to a list?

A. append() adds whatever it is given as one single element. If it is given a list, that list becomes one element sitting inside the outer list, and the outer list grows by exactly one position. extend() treats what it is given as a collection to be unpacked, and adds its elements one by one, so the outer list grows by as many positions as the inner list had elements. The two produce lists of different lengths and different shapes from the same argument.

To place an element somewhere other than the end, insert() is used. It takes two arguments — the index at which the new element should sit, and the element itself. Everything from that position onwards shifts one place to the right, as Fig. 9.8 shows.

>>> queue = ['Meera', 'Arjun', 'Kabir']
>>> queue.insert(1, 'Farida')
>>> queue
['Meera', 'Farida', 'Arjun', 'Kabir']

Fig. 9.8

Fig. 9.8 – Inserting an element into a list.

An index larger than the list simply places the element at the end rather than raising an error, and a negative index counts from the right. Both are conveniences you should use sparingly, since a reader of the program cannot tell at a glance where the element landed.

Table 9.8 – append(), extend() and insert()

Feature append() extend() insert()
Arguments taken One element One list An index and one element
Where the element goes At the end At the end At the given index
Increase in length Always 1 The length of the list supplied Always 1
Effect on other elements None None Those at and after the index shift right
If given a list as the element It becomes one nested element Its elements are added separately It becomes one nested element

assigning the result of a method that works in place What students write:

>>> stock = ['pen', 'pencil']
>>> stock = stock.append('eraser')
>>> print(stock)

What happens: the output is None. The method did add 'eraser' to the list, but it returned nothing, and that nothing was then assigned to stock — destroying the very list that had just been extended. The fix: call the method on its own line, stock.append('eraser'), and leave the name alone. Methods that change a list in place are used for their effect, not for their value.

9.4.3 Removing Elements from a List

Two methods take elements out. They differ in what you tell them: pop() is told where, and remove() is told what.

The method pop() removes the element at a given index and returns it, so the value is not lost — it can be printed or stored. Called with no argument at all, it removes and returns the last element.

>>> tokens = [21, 34, 47, 52, 60]
>>> tokens.pop()
60
>>> tokens
[21, 34, 47, 52]
>>> tokens.pop(1)
34
>>> tokens
[21, 47, 52]

This return value is what makes pop() the natural tool for handling items one at a time — a token called at a counter, a task taken off a pending list. The item is both removed and delivered.

The method remove() is given a value instead. It searches the list from the left and takes out the first element equal to that value. Later copies of the same value stay where they are.

>>> colours = ['red', 'green', 'blue', 'green']
>>> colours.remove('green')
>>> colours
['red', 'blue', 'green']

remove() returns nothing, and it is unforgiving if the value is not there.

>>> colours.remove('black')
ValueError: list.remove(x): x not in list

Similarly, pop() on an empty list raises IndexError: pop from empty list, and pop() with an index beyond the end raises IndexError: pop index out of range. In a program that keeps removing items, the safe habit is to check first — if colours: before popping, or if 'black' in colours: before removing.

9.4.4 Searching and Counting in a List

Section 9.2.3 showed how in answers is this value present? Two methods go further and answer where and how many.

The method index() returns the index of the first occurrence of a value. The method count() returns the number of times a value occurs.

>>> votes = ['A', 'B', 'A', 'C', 'A', 'B']
>>> votes.index('B')
1
>>> votes.count('A')
3
>>> votes.count('D')
0

The two behave quite differently when the value is missing.

>>> votes.index('D')
ValueError: 'D' is not in list

Q. Why does count() answer calmly with 0 while index() stops the program?

A. Because 0 is a truthful answer to “how many times does ‘D’ occur” — none. There is no equally truthful answer to “at what position does ‘D’ occur”, since every integer would be a lie. Rather than invent a position, Python refuses.

calling index() on a value that may be absent What students write:

>>> print("Position:", votes.index(choice))

What happens: if choice holds a value that is not in the list, the program stops with ValueError at that very line and nothing after it runs. The fix: test membership first, and provide for the other case.

>>> if choice in votes:
        print("Position:", votes.index(choice))
    else:
        print("Not found")
Counting distinct values

Put count(), in and append() together and a genuinely useful task becomes short: reporting each distinct value in a list along with how often it occurs.

listing each distinct value of a list with its number of occurrences Input: a list of values, which may contain repetitions. Output: each distinct value, printed once, with the number of times it occurs in the list. Method:

1. Create an empty list named seen.
2. Repeat steps 3 to 5 for every value in the given list.
3.    If the value is not present in seen, then
4.       add the value to seen, and
5.       display the value together with its count in the given list.
6. Report how many values seen now holds.

Why it works: a value is displayed only when it is missing from seen, which happens exactly once — at its first occurrence, since it is added to seen immediately afterwards. The count reported is taken from the original list, so later repetitions are included in the number even though they are never displayed again. Watch out: on an empty list nothing is displayed and the distinct count is 0. The values appear in order of first occurrence, not in sorted order.

reporting how often each periodical code was issued Problem: A list holds the codes of the periodicals issued at a library counter during one day, in the order they were issued, and a code may appear more than once. The program must display each distinct code once with the number of times it was issued, and then report how many distinct periodicals were involved.

# Displays each distinct code in a list along with the number of its occurrences

issued = ['P12', 'P45', 'P12', 'P78', 'P45', 'P12']
seen = []

for code in issued:
    if code not in seen:
        seen.append(code)
        print(code, ":", issued.count(code))

print("Distinct periodicals issued:", len(seen))

Sample output:

P12 : 3
P45 : 2
P78 : 1
Distinct periodicals issued: 3

Identifier description:

Table 9.9 – identifiers used in the periodical-count program, with their types and purposes

Identifier Type Purpose
issued list Codes issued during the day, in order of issue, with repetitions
seen list Codes already reported, used to avoid reporting one twice
code str Loop variable holding the code being examined

Notes: seen does double duty — it prevents repeated reporting, and its final length is the number of distinct codes, so no separate counter is needed. The method count() scans the whole list each time it is called, so this approach is comfortable for a day’s records but would be wasteful on a very large list; a dictionary, which you will meet later, is the tool for that scale.

9.4.5 Rearranging a List

Two methods change the order of the elements already present, without adding or removing anything.

The method reverse() turns the list end for end, in place.

>>> heights = [148, 162, 155, 170]
>>> heights.reverse()
>>> heights
[170, 155, 162, 148]

The method sort() arranges the elements in increasing order, also in place. Given the argument reverse = True, it arranges them in decreasing order instead.

>>> heights.sort()
>>> heights
[148, 155, 162, 170]
>>> heights.sort(reverse = True)
>>> heights
[170, 162, 155, 148]

Sorting requires that the elements can be compared with one another. A list of numbers or a list of strings sorts without difficulty; a list mixing the two raises TypeError, because Python has no rule for deciding whether a number comes before a word.

Strings sort in the order their characters have in the Unicode character set, and in that ordering every capital letter comes before every small letter. So sorting ['Kabir', 'arjun', 'Meera'] gives ['Kabir', 'Meera', 'arjun'], which looks wrong until you remember that 'a' does not mean “first letter” to Python — it means a particular character code, and that code is larger than the code of 'M'.

Alongside the method sort() there is the built-in function sorted(). It does the same arranging but leaves the original list exactly as it was, returning a new sorted list instead.

>>> readings = [31, 27, 35, 29]
>>> ordered = sorted(readings)
>>> ordered
[27, 29, 31, 35]
>>> readings
[31, 27, 35, 29]

The choice between them is a real decision, not a preference. If the original order carries information — readings in the order they were taken, students in roll number order — then destroying it to answer one question is a mistake, and sorted() is the correct tool.

Table 9.10 – sort() and sorted()

Feature sort() sorted()
Kind of operation A method, written after the list with a dot A built-in function, written with the list inside brackets
Effect on the original list Rearranged in place Left completely unchanged
What it returns Nothing A new list holding the sorted elements
Usual way of writing it heights.sort() ordered = sorted(heights)
When to prefer it The original order is not needed again The original order must be preserved

Q. What is stored in result after result = heights.sort()?

A. None. The list heights does get sorted, but sort() returns nothing, so result holds nothing useful. To obtain a sorted list as a value, use sorted() instead.

9.4.6 Deleting Elements Using del

There is a third way to take elements out of a list, and it is neither a method nor a function. del is a statement of the Python language, written before what is to be deleted.

Given an index, it removes that one element. Given a slice, it removes the whole range in one stroke — something neither pop() nor remove() can do.

>>> slots = [10, 20, 30, 40, 50, 60]
>>> del slots[2]
>>> slots
[10, 20, 40, 50, 60]
>>> del slots[1:3]
>>> slots
[10, 50, 60]

The second statement deleted the elements at indices 1 and 2 of the shortened list — 20 and 40 — leaving three elements behind. As always with slicing, the element at the end index is not included.

Given a list name with no index at all, del removes the name itself. The list is not emptied; the name simply ceases to exist, and using it afterwards is an error.

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

Table 9.11 – three ways of removing elements from a list

Feature del pop() remove()
Kind of operation A statement of the language A method A method
What you specify An index, a slice, or the whole name An index, or nothing for the last element The value to be removed
Number removed at once One element, or a whole slice One element One element, the first match only
Value handed back None The element removed None
Error when it fails IndexError for a bad index IndexError on an empty list or bad index ValueError if the value is absent
Typical use Clearing a range of elements Taking an item out to work with it Deleting a known value whose position is unknown

Start with a list of eight seat numbers. Using one operation at a time, (a) add a seat at the end, (b) add three more seats so that they appear as three separate elements, (c) insert a seat at the third position, (d) take out the last seat and print the number that came out, and (e) delete the first two seats in a single statement. Print the list after each step and check that its length changes by the amount you expected.

9.5 Nested Lists

Section 9.1 established that a list may hold elements of different types, and Section 9.4.2 showed what happens when append() is handed a list: the whole list settles into a single position. That was presented there as a trap. It is also a feature, and this section puts it to work.

When a list appears as an element of another list, it is called a nested list. The outer list still has its own elements and its own indices; it is only that one or more of those elements happen to be lists themselves.

>>> entry = ['Physics', 'PH-101', [18, 20, 17]]
>>> len(entry)
3

The length is 3, not 5. The three unit test marks occupy one position between them, as Fig. 9.9 shows.

Fig. 9.9

Fig. 9.9 – A nested list as a single element.

NoteDefinition

A nested list is a list that appears as an element of another list. It occupies a single position in the outer list, however many elements it holds itself.

This is the natural shape for a record whose parts are not all single values. A subject has one name, one code, and several marks; forcing the marks into separate positions would mean the outer list changes length whenever the number of tests changes, and no position would mean anything on its own.

Reaching an element inside a nested list

One index takes you to the nested list. A second index, written immediately after the first, takes you to an element within it.

>>> entry[2]
[18, 20, 17]
>>> entry[2][1]
20
>>> len(entry[2])
3

Read entry[2][1] from left to right, as two steps rather than one strange operation. entry[2] produces the list [18, 20, 17]; the [1] that follows is then applied to that list and produces its second element. The general form is list_name[i][j], where i chooses the nested list and j chooses an element inside it.

using two indices where the element is not a list What students write:

>>> entry = ['Physics', 'PH-101', [18, 20, 17]]
>>> entry[2][1][0]

What happens: Python stops with TypeError: 'int' object is not subscriptable. The first two indices give the integer 20, and an integer has no positions inside it to index. The fix: use as many indices as there are levels of nesting, and no more. Here there are two levels, so two indices reach a mark.

A nested list as a table

The more powerful use of nesting is a list in which every element is a list of the same length. Such a structure is a table: each nested list is one row, and each position within a row is one column. The unit test scores of four students in three tests are set out in Table 9.12.

Table 9.12 – unit test scores of four students, out of 50 in each test

Student Test 1 Test 2 Test 3
1 35 42 38
2 28 31 40
3 45 39 44
4 30 36 33

In Python this table becomes a list of four nested lists. Writing each row on its own line is a habit worth forming — Python allows a list to run across several lines as long as the brackets are not closed, and the layout makes the table visible in the program text.

>>> scores = [[35, 42, 38],
              [28, 31, 40],
              [45, 39, 44],
              [30, 36, 33]]
>>> scores[2]
[45, 39, 44]
>>> scores[2][1]
39

The first index is the row, the second is the column, and Fig. 9.10 shows the pair meeting at one value.

Fig. 9.10

Fig. 9.10 – Reaching an element in a table.

Q. For this table, what does len(scores) return — 4 or 12?

A. 4. The outer list has four elements, and each of them is a row. The number of values in a row is len(scores[0]), which is 3, so the table holds \(4 \times 3 = 12\) numbers in all — but len() counts only the elements of the list it is given.

Traversing a table

A single loop over a table visits rows, not values. To reach every value, the loop over rows must contain a second loop over the positions within a row — one loop for each level of nesting.

totalling each row of a table stored as a nested list Input: a list whose elements are lists of numbers, one nested list per row. Output: the total of each row, and the total of all the rows together. Method:

1. Set the overall total to 0.
2. Repeat steps 3 to 6 for every row index i of the outer list.
3.    Set the row total to 0.
4.    Repeat step 5 for every column index j of the row at index i.
5.       Add the value at row i, column j to the row total.
6.    Add the row total to the overall total, and display the row total.
7. Display the overall total.

Why it works: the inner loop runs once for every position in one row, so it accumulates that row completely before the outer loop moves on; the row total is reset at step 3, so no row inherits the previous row’s figure. Watch out: the number of columns is taken from the row being examined, not assumed, so rows of unequal length are handled correctly. If a row is an empty list, its total is 0 and nothing goes wrong.

totalling the unit test scores of each student Problem: A nested list holds the unit test scores of four students, one nested list per student, with three test scores in each. The program must display the total scored by each student and the total scored by all of them together.

# Totals each row of a nested list of unit test scores

scores = [[35, 42, 38],
          [28, 31, 40],
          [45, 39, 44],
          [30, 36, 33]]

grand_total = 0

for i in range(len(scores)):
    total = 0
    for j in range(len(scores[i])):
        total = total + scores[i][j]
    grand_total = grand_total + total
    print("Student", i + 1, "total:", total)

print("Total of all students:", grand_total)

Sample output:

Student 1 total: 115
Student 2 total: 99
Student 3 total: 128
Student 4 total: 99
Total of all students: 441

Identifier description:

Table 9.13 – identifiers used in the row-totalling program, with their types and purposes

Identifier Type Purpose
scores list The whole table, one nested list per student
i int Row index, selecting one student
j int Column index, selecting one test within that student’s row
total int Total for the row currently being added
grand_total int Running total of every row totalled so far

Notes: total is set to 0 inside the outer loop, not before it, because each student needs a fresh count; moving that one line above the loop would make every printed figure a running sum instead. The inner loop could be replaced by total = sum(scores[i]), since a row is an ordinary list of numbers — shorter, and worth using once the two-index form is understood.

The trace below follows the outer loop, showing the three values the inner loop adds during each pass.

the row-totalling program on the table of Table 9.12 | Outer iteration | i | Values added by the inner loop | total at the end of the inner loop | grand_total after | Line printed | |—|—|—|—|—|—| | before the loop | — | — | — | 0 | — | | 1 | 0 | 35, 42, 38 | 115 | 115 | Student 1 total: 115 | | 2 | 1 | 28, 31, 40 | 99 | 214 | Student 2 total: 99 | | 3 | 2 | 45, 39, 44 | 128 | 342 | Student 3 total: 128 | | 4 | 3 | 30, 36, 33 | 99 | 441 | Student 4 total: 99 |

Result: the four totals are printed as they are computed, and 441 is printed at the end. The trace shows the two loops working at different speeds — the inner loop completes three additions for every single step the outer loop takes, which is why a table of four rows and three columns costs twelve additions altogether.

Set up a nested list holding the daily rainfall recorded at three weather stations over four days, one nested list per station. Write statements that print (a) all the readings of the second station, (b) the reading of the third station on the first day, and (c) the number of days for which readings were kept. Then write a traversal that prints the highest reading recorded by each station.

9.6 Copying Lists

Programs often need a second version of a list — the original readings kept safe while a sorted arrangement is examined, a backup taken before a risky series of deletions. The obvious way to make one is to assign the list to a second name. It is also the wrong way, and this section explains why.

Everything here rests on one idea already met in Section 9.1.3: a name does not contain a list. A name refers to a list that exists somewhere in memory. Two names can perfectly well refer to the same list, and assignment is exactly what arranges that.

9.6.1 Assignment Creates an Alias

When one list name is assigned to another, no new list is built. Python simply makes the second name point at the list the first name already points at. The second name is then an alias — another name for the same object, as drawn in Fig. 9.11.

>>> list1 = [11, 22, 33]
>>> list2 = list1
>>> list2
[11, 22, 33]

Fig. 9.11

Fig. 9.11 – Assignment creates an alias to the same list.

NoteDefinition

An alias is a second name that refers to the same object as an existing name. A change made through either name is a change to the one object, and is therefore visible through both.

Python can be asked directly whether two names refer to the same object, using the is operator.

>>> list2 is list1
True

The consequence appears the moment either list is changed. Appending through list1 alters the one list that exists, and list2 — which was never touched — reports the change.

>>> list1.append(44)
>>> list2
[11, 22, 33, 44]

It works in the other direction too, because neither name is the original in any meaningful sense.

>>> list2.remove(11)
>>> list1
[22, 33, 44]

Q2. If you write one list name equal to another, why can changing one of them change the other as well?

A. Because the assignment does not copy anything. It makes the second name refer to the same list object that the first name refers to, so there is only one list in memory with two names attached to it. Any method that changes a list in place — append(), remove(), sort() — changes that single object, and both names then describe the changed list. Nothing has been duplicated, so there is nothing to stay behind unchanged.

expecting assignment to make a backup What students write:

>>> original = [58, 91, 47]
>>> backup = original
>>> original.sort()
>>> backup

What happens: backup shows [47, 58, 91]. The sort was meant to leave a copy in the earlier order, but there was never a copy — only a second name for the list that got sorted. Nothing reports an error, so the loss of the original order goes unnoticed until the wrong figures appear later. The fix: build a genuine second list, using one of the three ways given in Section 9.6.2.

Aliasing is not a defect. It is what makes it possible to hand a large list to a function without duplicating it, which Section 9.7 takes up. It only becomes a problem when a copy was intended and not made.

9.6.2 Making a Distinct Copy

To obtain a list that is separate from the original, Python offers three ways. All three produce a new list object holding the same elements, so a change to one leaves the other alone, as Fig. 9.12 shows.

Fig. 9.12

Fig. 9.12 – Copying creates a distinct list object.

The first way uses a slice with both indices left out, which Section 9.2.4 showed produces a slice containing every element. Since slicing always builds a new list, the result is a copy.

>>> original = [11, 22, 33]
>>> duplicate = original[:]
>>> duplicate is original
False
>>> duplicate.append(44)
>>> original
[11, 22, 33]

The second way uses the built-in function list(), which builds a list from any sequence handed to it — including another list.

>>> duplicate = list(original)
>>> duplicate is original
False

The third way uses the copy() function of the standard library module copy, which must be imported before use.

>>> import copy
>>> duplicate = copy.copy(original)
>>> duplicate
[11, 22, 33]
>>> duplicate is original
False

Table 9.14 – three ways of making a distinct copy of a list

Way How it is written Needs an import
Slicing duplicate = original[:] No
The list() function duplicate = list(original) No
The copy() function duplicate = copy.copy(original) Yes — import copy

For an ordinary list of numbers or strings the three are interchangeable, and the slice form is the shortest. The copy module earns its keep on structures more elaborate than a plain list, where the extra facilities it provides matter.

Same contents, different objects

Two lists can hold equal elements without being the same object, and Python has separate tests for the two questions. The operator == compares contents; the operator is compares identity.

>>> duplicate = list(original)
>>> duplicate == original
True
>>> duplicate is original
False

Table 9.15 – assignment and copying compared

Feature Assignment, list2 = list1 Copying, list2 = list1[:]
Number of list objects afterwards One Two
What the second name refers to The same list as the first name A new list of its own
Result of list2 is list1 True False
Result of list2 == list1 True True, until one of them is changed
Effect of list2.append(50) on list1 list1 shows the new element too list1 is unaffected
When to use it Referring to one list under another name Keeping the original safe from changes

Q. duplicate == original gives True. Does that prove they are the same list?

A. No. It proves only that their elements match, one for one. Two separate lists holding identical values are equal but not identical. Only is answers the question of sameness.

A copy made in any of these three ways duplicates the outer list, but the elements themselves are shared rather than rebuilt. For a list of numbers or strings this makes no observable difference, since those cannot be altered anyway. For a nested list of the kind met in Section 9.5 it does: the copy has its own outer list, yet its rows are the very same row objects, so a value changed inside a row shows up in both. The copy module provides a separate function for copying every level independently when that is required.

Using a copy in practice

Marks entered in roll number order carry information in that order, and sorting them destroys it. A copy lets both arrangements exist at once.

displaying marks in entry order and in decreasing order Problem: A list holds the marks of five students in the order of their roll numbers. The program must display the marks in that order, display them again in decreasing order, and report the three highest marks — all without disturbing the original list.

# Sorts a copy of a list so that the original order survives

marks = [58, 91, 47, 76, 63]

ranked = marks[:]
ranked.sort(reverse = True)

print("Marks in roll number order:", marks)
print("Marks in decreasing order:", ranked)
print("Top three marks:", ranked[:3])

Sample output:

Marks in roll number order: [58, 91, 47, 76, 63]
Marks in decreasing order: [91, 76, 63, 58, 47]
Top three marks: [91, 76, 63]

Identifier description:

Table 9.16 – identifiers used in the marks-ranking program, with their types and purposes

Identifier Type Purpose
marks list Marks in roll number order, to be preserved unchanged
ranked list A separate copy of the marks, sorted in decreasing order

Notes: The copy is made before sort() is called, because sort() works in place and there is no way to recover the earlier order afterwards. The same result can be had in one statement with sorted(marks, reverse = True), which builds its own new list; the two-step form is shown here because it makes the copying visible. Once ranked exists, ranked[:3] gives the top three without a loop, since the list is already in decreasing order.

Create a list of six seat numbers and make a second name refer to it by plain assignment. Delete one element through the second name, print both names, and explain the result in one sentence. Now repeat the whole exercise, this time making a proper copy first, and confirm that only one of the two lists changes.

9.7 List as Argument to a Function

A function is written once and called wherever its work is needed. The values it works on are handed to it at the call: the value written in the call is the argument, and the name it arrives under inside the function is the parameter. For a number or a string this exchange holds no surprises — the function receives the value, uses it, and the caller’s variable is unaffected by anything the function does.

A list behaves differently, and the reason is already familiar. Section 9.6.1 showed that assigning a list to a second name creates an alias rather than a copy. Passing a list to a function does exactly the same thing: the parameter becomes another name for the caller’s list. What travels into the function is a reference to the one existing list, not a duplicate of it, as Fig. 9.13 shows.

Fig. 9.13

Fig. 9.13 – A list parameter refers to the caller’s list.

NoteDefinition

When a list is passed to a function, the parameter refers to the same list object as the argument. No copy is made, so the function works on the caller’s list itself.

This one fact explains everything in this section, including the apparent contradiction that a function can sometimes change the caller’s list and sometimes cannot.

9.7.1 Changing the Elements of the Original List

If the function alters the elements of the list — assigning through an index, appending, removing, sorting — it is altering the one list that exists. The caller sees the change, because the caller’s name refers to that same list.

Consider a set of readings that must be trimmed to an upper limit before being used further. The work is a natural candidate for a function, since it will be needed for several sets of readings.

limiting every element of a list to a given maximum Input: a list of numbers, and the maximum value that is to be allowed. Output: no value is returned; every element of the list greater than the maximum is replaced by the maximum. Method:

1. Repeat steps 2 to 3 for every index i of the list.
2.    If the element at index i is greater than the given maximum, then
3.       store the maximum at index i in place of that element.

Why it works: each position is examined exactly once, and a position is written to only when its value breaks the limit. After the last position, no element can exceed the maximum, and no element that was already within the limit has been touched. Watch out: the list is altered in place, so the earlier values are gone once the function returns. If they are needed afterwards, the caller must copy the list before making the call.

limiting a list of readings to an upper value through a function Problem: A list holds five recorded readings. Using a function, every reading above a stated limit must be replaced by that limit, and the list held by the caller must show the trimmed values afterwards.

# Replaces every reading above a stated limit by the limit itself

def cap(readings, limit):
    for i in range(len(readings)):
        if readings[i] > limit:
            readings[i] = limit

temps = [31, 48, 27, 55, 39]
print("Readings before the call:", temps)

cap(temps, 45)
print("Readings after the call:", temps)

Sample output:

Readings before the call: [31, 48, 27, 55, 39]
Readings after the call: [31, 45, 27, 45, 39]

Identifier description:

Table 9.17 – identifiers used in the reading-limiting program, with their types and purposes

Identifier Type Purpose
temps list The readings held by the caller, passed as the argument
readings list The parameter, another name for the same list inside the function
limit int The largest value that any reading is allowed to take
i int Loop variable holding the index being examined

Notes: The function returns nothing, and it does not need to. Its whole purpose is the change it makes to the list it was given, which is why the call stands on a line of its own rather than on the right of an assignment. Writing temps = cap(temps, 45) would replace the trimmed list with None, the same mistake as the one met with append() in Section 9.4.2.

the function cap on the list [31, 48, 27, 55, 39] with limit equal to 45 | Iteration | i | readings[i] | Is it greater than 45? | The list after this iteration | |—|—|—|—|—| | 1 | 0 | 31 | No | [31, 48, 27, 55, 39] | | 2 | 1 | 48 | Yes | [31, 45, 27, 55, 39] | | 3 | 2 | 27 | No | [31, 45, 27, 55, 39] | | 4 | 3 | 55 | Yes | [31, 45, 27, 45, 39] | | 5 | 4 | 39 | No | [31, 45, 27, 45, 39] |

Result: the caller’s list ends as [31, 45, 27, 45, 39]. The trace shows the change happening inside the function on a list the function never created — after iteration 2 the second element is already 45, and temps outside would report the same thing if it could be printed at that moment.

9.7.2 Reassigning the List Inside the Function

Now consider a function that does not change the elements but assigns a whole new list to the parameter.

>>> def replace(readings):
        readings = [0, 0, 0]
        print("Inside the function:", readings)

>>> temps = [31, 48, 27]
>>> replace(temps)
Inside the function: [0, 0, 0]
>>> temps
[31, 48, 27]

Nothing outside the function has changed. The assignment did not alter the list that readings referred to; it made readings refer to a different list, one newly built inside the function. The caller’s name temps was never involved and still points where it always did, as Fig. 9.14 shows.

Fig. 9.14

Fig. 9.14 – Reassigning the parameter inside the function.

The distinction is not about which statement looks more drastic. It is about whether the statement changes the contents of an object or changes what a name refers to.

Table 9.18 – changing the elements and reassigning the parameter

Feature Changing the elements Reassigning the parameter
Typical statement inside the function readings[i] = 45, readings.append(50), readings.sort() readings = [0, 0, 0], readings = readings + [50]
What the statement acts on The list object itself The parameter name only
Number of list objects involved One Two — the caller’s, and a new one
Effect on the caller’s list The change is visible outside No effect at all
How the result reaches the caller Automatically, through the shared list Only by return, and assignment at the call

Q. Inside a function, which of readings.sort() and readings = sorted(readings) leaves the caller’s list in its original order?

A. The second. sort() rearranges the shared list, so the caller sees it sorted. sorted() builds a new list and the assignment attaches the parameter to that new list, leaving the caller’s list untouched — and leaving the sorted version stranded inside the function unless it is returned.

Q4. When a list is handed over to a function, can the function change it permanently?

A. Yes, if it changes the elements of the list — through an index assignment, or through a method such as append(), remove() or sort(). The function and the caller are working on the same list object, so such a change is permanent and visible outside. But if the function assigns a whole new list to its parameter, the caller’s list is untouched: the parameter has merely been pointed at something else. Whether the change survives depends entirely on which of these two things the function does.

Returning a new list instead

Changing the caller’s list is sometimes exactly what is wanted, and sometimes a hazard — a function that quietly reorders data its caller still needs is hard to debug. The alternative is to leave the argument alone and build a fresh list to return.

>>> def doubled(readings):
        result = []
        for r in readings:
            result.append(r * 2)
        return result

>>> temps = [31, 48, 27]
>>> new = doubled(temps)
>>> new
[62, 96, 54]
>>> temps
[31, 48, 27]

Here result is created inside the function and belongs to it alone, so nothing the loop does can reach the caller’s data. The value comes back through return, and the caller decides what to do with it. Between the two designs, prefer this one unless changing the original is the stated purpose of the function — and when it is the stated purpose, say so in the function’s opening comment, so that a reader of the call is not taken by surprise.

Practical link: This assignment exercises passing a list to a function and the difference between changing a list in place and returning a new one. File it under Python programs in the practical report file. Write two functions that work on a list of daily attendance counts. The first must take the list and a number, and add that number to every element of the list in place, returning nothing. The second must take the same list and return a new list holding only the counts above a given value, leaving the original untouched. Call both from the same program, printing the original list after each call so that the difference is visible. Test your program with a list in which no count at all is above the given value.

9.8 List Manipulation

Every operation this chapter has introduced was shown on its own, on a list that was already there. Real programs are not like that. They build the list from data that arrives while they run, ask questions of it, change it in response to what the user wants, and go on doing so until the user stops them.

This section puts the pieces together in two complete programs. Nothing new is introduced; what is new is the shape of the whole, and the reasoning that decides which operation belongs where.

Building a list from data as it arrives

A list does not have to be typed into the program. The usual pattern is to start with an empty list and use append() once for each value read, so the list ends up exactly as long as the data requires.

Once the list exists, questions about it can be answered. A common one — how many values lie above the average — needs two passes over the data: the average cannot be known until every value has been seen, and the comparison cannot be made until the average is known.

counting how many values in a list exceed the average Input: a count of how many values are to be read, and the values themselves. Output: the list of values, their average, and the number of values greater than the average. Method:

1. Create an empty list.
2. Repeat step 3 as many times as the count requires.
3.    Read one value and add it to the end of the list.
4. Divide the total of the list by the number of its elements to obtain the average.
5. Set a tally to 0.
6. Repeat step 7 for every value in the list.
7.    If the value is greater than the average, increase the tally by 1.
8. Display the list, the average and the tally.

Why it works: step 3 leaves the list holding exactly the values read, in the order they were read, so the total at step 4 is the total of all of them and the division gives the true average. The comparison in step 7 is then made against a figure that is already final, so no value is judged against a partial average. Watch out: a count of 0 leaves an empty list, and step 4 divides by zero. A value exactly equal to the average is not counted, because the test asks for values strictly greater.

counting how many marks lie above the class average Problem: The program must read a stated number of marks from the user, store them in a list, compute their average, and report how many of the marks are greater than that average. The list and the average must both be displayed.

# Reads marks into a list and reports how many of them are above the average

def read_marks(count):
    values = []
    for i in range(count):
        mark = int(input("Enter mark " + str(i + 1) + ": "))
        values.append(mark)
    return values

def count_above(values, limit):
    tally = 0
    for mark in values:
        if mark > limit:
            tally = tally + 1
    return tally

n = int(input("How many marks: "))
marks = read_marks(n)

average = sum(marks) / len(marks)

print("Marks entered:", marks)
print("Average:", average)
print("Number of marks above the average:", count_above(marks, average))

Sample output:

How many marks: 5
Enter mark 1: 48
Enter mark 2: 62
Enter mark 3: 55
Enter mark 4: 71
Enter mark 5: 39
Marks entered: [48, 62, 55, 71, 39]
Average: 55.0
Number of marks above the average: 2

Identifier description:

Table 9.19 – identifiers used in the above-average program, with their types and purposes

Identifier Type Purpose
n int How many marks are to be read
marks list The marks, in the order they were entered
values list Parameter and local list of read_marks(), returned to the caller
mark int One mark, while it is being read or examined
average float The average of all the marks
limit float Parameter of count_above(), holding the value to compare against
tally int Count of marks found to be above the limit

Notes: read_marks() builds a new list and returns it, so the caller receives it by assignment — the approach discussed at the end of Section 9.7.2. count_above() only reads the list it is given and changes nothing, which is why it is safe to call it with marks directly. Note that the mark of 55 is not counted: the average is exactly 55.0 and the test is >, not >=. That is a decision, not an accident, and a program that means to include such a student must say so.

Q. Why is the average printed as 55.0 and not as 55?

A. Because the division operator / always produces a floating-point value, even when the division comes out exact. The value is right; it is only being displayed with the decimal part that its type carries.

A program that keeps working until told to stop

The second pattern is the menu-driven program. The list is displayed, a menu of operations is offered, the user chooses one, the program carries it out — and then the whole cycle begins again. Only an explicit choice to exit ends it. Fig. 9.15 shows the cycle.

Fig. 9.15

Fig. 9.15 – Execution flow of a menu-driven program.

The loop that drives this is written as while True, which never becomes false by itself, together with a break that leaves it when the exit option is chosen. Writing a condition at the top would be awkward here, because at the moment the loop begins there is nothing yet to test.

maintaining a list through a menu of operations Input: an existing list, and the user’s choices and values entered during the run. Output: the list, displayed after every operation, and left in its final state when the user exits. Method:

1. Repeat steps 2 to 5 indefinitely.
2.    Display the current list and the menu of operations.
3.    Read the user's choice.
4.    If the choice is to exit, leave the loop.
5.    Otherwise carry out the operation the choice names, reading any value it needs.

Why it works: every operation leaves the list in a valid state, and step 2 runs again before the next choice is read, so the user always chooses on the basis of what the list actually holds at that moment. Watch out: a value to be removed may not be present, so membership must be checked before removal or the program stops with ValueError. A choice outside the menu must be rejected rather than ignored silently.

maintaining a list of seat numbers through a menu Problem: A list holds the seat numbers allotted in a school bus. The program must repeatedly offer the user a menu to add a seat at the end, insert a seat at a given position, remove a seat by its number, or sort the list, displaying the list before every choice and stopping only when the user chooses to exit.

# Menu driven program to maintain a list of allotted seat numbers

seats = [12, 25, 31, 44]

while True:
    print("\nCurrent list:", seats)
    print("1. Add a seat at the end")
    print("2. Insert a seat at a position")
    print("3. Remove a seat by its number")
    print("4. Sort the list")
    print("5. Exit")

    choice = int(input("Enter your choice (1-5): "))

    if choice == 1:
        number = int(input("Seat number to add: "))
        seats.append(number)
        print("Seat added")
    elif choice == 2:
        number = int(input("Seat number to insert: "))
        place = int(input("Position: "))
        seats.insert(place, number)
        print("Seat inserted")
    elif choice == 3:
        number = int(input("Seat number to remove: "))
        if number in seats:
            seats.remove(number)
            print("Seat removed")
        else:
            print("That seat number is not in the list")
    elif choice == 4:
        seats.sort()
        print("List sorted")
    elif choice == 5:
        print("Closing the register")
        break
    else:
        print("Invalid choice")

Sample output:


Current list: [12, 25, 31, 44]
1. Add a seat at the end
2. Insert a seat at a position
3. Remove a seat by its number
4. Sort the list
5. Exit
Enter your choice (1-5): 1
Seat number to add: 58
Seat added

Current list: [12, 25, 31, 44, 58]
1. Add a seat at the end
2. Insert a seat at a position
3. Remove a seat by its number
4. Sort the list
5. Exit
Enter your choice (1-5): 3
Seat number to remove: 25
Seat removed

Current list: [12, 31, 44, 58]
1. Add a seat at the end
2. Insert a seat at a position
3. Remove a seat by its number
4. Sort the list
5. Exit
Enter your choice (1-5): 5
Closing the register

Identifier description:

Table 9.20 – identifiers used in the seat register program, with their types and purposes

Identifier Type Purpose
seats list The seat numbers currently allotted
choice int The menu option entered by the user
number int A seat number supplied for adding, inserting or removing
place int Position at which a seat number is to be inserted

Notes: The membership test before remove() is not decoration — without it, one mistyped seat number ends the whole session. The insert option is deliberately left unguarded so that you can see what happens: a position beyond the end of the list places the seat at the end instead of reporting an error, which is convenient but hides a typing mistake from the user. Adding a check of place against len(seats) is a worthwhile improvement to attempt.

assuming the user will type a number What students write:

choice = int(input("Enter your choice (1-5): "))

What happens: if the user types a letter, int() cannot convert it and the program stops at once with ValueError: invalid literal for int() with base 10: 'a'. The else branch never gets a chance to run, because the failure happens before choice is ever compared. The fix: the else branch handles a wrong number, such as 9, and is still needed. Handling a wrong kind of input requires exception handling, which is taken up in a later chapter; until then, state in the prompt exactly what is expected.

Q. Why is the list displayed at the top of the loop rather than at the end?

A. So that the user sees the list before choosing, including the very first time round. Displaying it at the end would show the result of the last operation but leave the user choosing blind at the start of the run.

Extend the seat register with a sixth option that reports how many seats are currently allotted and the lowest and highest seat numbers, without adding a loop of your own. Then run the program, choose option 2 with a position far larger than the length of the list, and record where the seat actually lands.

9.9 Summary

What a list is
  1. A list is an ordered sequence of elements written inside square brackets and separated by commas. It holds many values under one name, so the amount of data a program handles no longer decides how many variables it must declare.

  2. Ordered means that every element has a fixed position that is remembered; it does not mean sorted.

  3. The elements of one list may be of different data types, because a list is defined by the order it keeps and not by what its elements are. This is the difference from a string, whose elements can only be characters.

  4. Positions are numbered from 0 for the first element, and from −1 for the last element counting backwards. The highest forward index is therefore always one less than the length.

  5. Asking for an element at a position that does not exist raises IndexError.

  6. A list is mutable: an element can be replaced through its index, and the same list object is altered where it stands, without a new list being created. A string is immutable, and refuses such an assignment.

Operations on a whole list
  1. Concatenation with + joins two lists into a new list, leaving both originals unchanged. Both operands must be lists.

  2. Repetition with * produces a new list in which the elements are repeated a stated number of times, which is the usual way of setting up a list of counters that all start alike.

  3. The membership operators in and not in report whether a value occurs in a list, returning True or False. On a list the match must be against a whole element, unlike on a string, where a part of the text is enough.

  4. Slicing extracts a part of a list into a new list. The starting index is included and the ending index is excluded, so the number of elements taken is the difference between them.

  5. Omitting an index in a slice means “from the beginning” or “to the end”; a third number gives the step, and a step of −1 produces the list in reverse order.

  6. A single index beyond the end of a list is an error, but a slice beyond the end is not — a slice returns whatever lies between its two boundaries, which may be a shorter list or an empty one.

Traversing a list
  1. Traversal is the visiting of every element exactly once, in order. It is what allows a question to be asked about all the data rather than one element of it.

  2. A for loop written over the list itself gives the elements directly and is the safest choice when the position does not matter.

  3. A for loop written over range(len(...)) gives the indices instead, and is needed when the position matters or when neighbouring elements must be compared. Taking the length from the list rather than typing a fixed number keeps the loop correct when the list changes size.

  4. A while loop requires the counter to be created, tested and increased by the programmer, and an omitted increase produces an endless loop. In exchange, it can stop as soon as its condition fails, which suits searches for the first element satisfying a condition.

Methods and built-in functions
  1. A built-in function is written with the list inside its brackets; a method is written after the list, joined by a dot, and acts on that list. The built-in functions used with lists are len(), min(), max(), sum(), list() and sorted().

  2. Most list methods change the list in place and return nothing. Assigning the result of such a method to a name destroys the list and stores None instead.

  3. append() adds one element at the end, and a list given to it becomes a single nested element. extend() adds each element of the list given to it separately. insert() places an element at a stated index, shifting the rest to the right.

  4. pop() removes an element by its position and returns it, so the value can still be used; called without an argument it removes the last element. remove() removes the first element equal to a stated value and returns nothing, raising ValueError if the value is absent.

  5. index() gives the position of the first occurrence of a value and raises ValueError if there is none; count() gives the number of occurrences and simply reports 0 when there are none.

  6. reverse() and sort() rearrange the list in place, sort() accepting an argument that reverses the order. The built-in function sorted() does the same arranging but returns a new list and leaves the original untouched, which is the right choice when the original order carries information.

  7. del is a statement, not a method. It removes an element by index, a whole range by slice, or the name itself, after which using that name is an error.

Nested lists
  1. A list appearing as an element of another list is a nested list. It occupies one position in the outer list however many elements it holds, so the length of the outer list counts it once.

  2. Two indices are used to reach inside it: the first selects the nested list and the second selects an element within it. Applying more indices than there are levels of nesting raises TypeError.

  3. A list in which every element is a nested list of equal length behaves as a table, the first index naming the row and the second the column. Traversing such a table needs one loop for each level of nesting.

Copying, and passing to a function
  1. Assigning one list name to another does not copy the list. It creates an alias — a second name for the same object — so a change made through either name is visible through both. The is operator reveals this, since it compares identity rather than contents.

  2. A distinct copy is made by taking a full slice, by passing the list to list(), or by using copy() from the copy module. All three build a new list, so the two can then be changed independently. All three copy the outer list only, leaving nested lists shared.

  3. When a list is passed to a function, the parameter becomes an alias of the argument; no copy is made. Changing the elements through the parameter therefore changes the caller’s list, and the change survives the call.

  4. Assigning a whole new list to the parameter does the opposite: it breaks the link, leaves the caller’s list untouched, and discards the new list unless it is returned and assigned by the caller.

  5. The choice between changing a list in place and returning a new one is a design decision. Changing in place is economical but invisible at the point of call; returning a new list is explicit and safe but costs a second list in memory.

9.10 Key Terms

  1. alias — a second name that refers to the same list object as an existing name, so that a change made through either name is seen through both.

  2. argument — the value actually supplied to a function when it is called.

  3. concatenation — joining two lists end to end with the + operator, producing a new list and leaving both originals unchanged.

  4. element — one of the values stored in a list.

  5. immutable — describing an object whose contents cannot be changed after it has been created, as a string cannot.

  6. in place — describing an operation that alters an existing object instead of building a new one.

  7. index — the position of an element in a list, counted from 0 at the left or from −1 at the right.

  8. iteration — one pass of a loop, during which one element is dealt with.

  9. LISP — list processing, the name of the early programming language built around the list as its central structure.

  10. list — an ordered sequence of elements, written in square brackets and separated by commas, whose elements may be of different types and may be changed after the list is created.

  11. membership operator — the operator in, and its partner not in, which report whether a value occurs as an element of a list.

  12. method — a function belonging to an object, called by writing the object, a dot, and the method name with its arguments.

  13. mutable — describing an object whose contents can be changed after it has been created, as a list can.

  14. nested list — a list that appears as an element of another list, occupying a single position in the outer list.

  15. parameter — the variable named in a function header, which receives the argument when the function is called.

  16. repetition — producing a new list in which the elements of an existing list appear a stated number of times, using the * operator.

  17. sequence — a collection whose elements have fixed positions, so that any element can be reached by its position.

  18. slice — the new list produced by a slicing operation.

  19. slicing — extracting a part of a list by giving a starting index, an ending index that is not included, and optionally a step.

  20. traversal — visiting every element of a list exactly once, in order, so that some operation can be carried out on each.

9.11 Points to Ponder

  1. A string and a list are both ordered sequences that can be indexed and sliced, yet one refuses to be altered and the other invites it. If mutability is so useful, what does a language gain by keeping an immutable sequence type at all?

  2. Counting positions from 0, and excluding the end index of a slice, both feel unnatural when you first meet them. Taken together, what do these two choices make simpler that counting from 1 would make harder?

  3. Assignment gives an alias, while a full slice gives a copy. Python could have made every assignment copy the list and spared students a great deal of confusion. What would that decision have cost, and would the confusion really have disappeared?

  4. Asked for a value that is not there, count() answers 0 while index() stops the program. Both are refusals to invent an answer, but one is quiet and one is loud. When is a loud failure the more helpful behaviour?

  5. A function that changes the elements of the list passed to it leaves no sign of having done so at the point where it is called. Is that a weakness of the language, or a responsibility of the programmer who names the function?

  6. append() treats the list given to it as one thing, and extend() treats it as many. Could a single method have worked out which was meant from the argument itself, and what would a programmer lose if it did?

9.12 Objective Questions

  1. What is the output of the following statement? [1 mark]
print(len([10, [20, 30], 40]))
a. 3
b. 4
c. 5
d. An error is reported
  1. Which one of the following leaves the original list completely unchanged? [1 mark]
    1. sort()
    2. reverse()
    3. sorted()
    4. append()
  2. For values = [5, 10, 15, 20, 25], what does values[1:4:2] produce? [1 mark]
    1. [10, 15]
    2. [10, 20]
    3. [5, 15]
    4. [10, 15, 20]
  3. For values = [1, 2, 3], which statement raises IndexError? [1 mark]
    1. values[-3]
    2. values[2]
    3. values[3]
    4. values[1:9]
  4. Assertion (A): A single list can hold an integer, a string and a floating-point number at the same time. Reason (R): A list stores its elements in the order in which they are written. [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.
  5. Assertion (A): After list2 = list1, appending an element to list1 also changes list2. Reason (R): The statement list2 = list1 builds a new list holding the same elements. [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. Match each method in Column A with what it does, in Column B. [1 mark]
Column A Column B
a. append() i. Adds each element of a given list at the end
b. extend() ii. Returns the number of occurrences of a value
c. insert() iii. Adds one element at the end
d. pop() iv. Removes the element at a given position and returns it
e. count() v. Adds one element at a stated position
  1. Fill in the blank: The built-in function ____________ returns a new sorted list and leaves the list given to it unchanged. [1 mark]

  2. State whether the following is true or false: del can remove more than one element of a list in a single statement. [1 mark]

  3. What is the output of the following fragment? [1 mark]

values = [3, 6, 9]
values.append([12, 15])
print(len(values))

9.13 Check Your Concepts

  1. Define a mutable object. Name one mutable and one immutable data type in Python. [2 marks]

  2. What is an alias? State one situation in which making an alias instead of a copy makes a program give a wrong answer. [2 marks]

  3. Write a single statement that creates a list of eight zeros, and state which operator you have used. [2 marks]

  4. Differentiate between append() and extend(), giving one example of each. [3 marks]

  5. Differentiate between pop() and remove() in three points. [3 marks]

  6. Write a function that receives a list of numbers and returns how many of them are greater than 100. [3 marks]

  7. A list holds the finishing times of runners arranged in order of lane number. Explain why sorting this list to find the fastest time destroys information the program may still need, and describe how the difficulty is avoided. [4 marks]

  8. Write a function that receives a nested list in which each element is a list of numbers, and returns a new list holding the total of each inner list. [4 marks]

  9. Write a function that receives a list and a value, and removes every occurrence of that value from the list it was given, without building a new list. Explain why the change is visible to the calling program, and state what your function returns. [5 marks]

9.14 Output & Debugging

  1. Predict the output of the following fragment. [2 marks]
data = [4, 8, 12, 16, 20]
print(data[1:4])
print(data[::-2])
print(data[4:1])
  1. Predict the output of the following fragment. [2 marks]
items = ['a', 'b', 'a', 'c', 'a']
print(items.count('a'), items.index('c'))
items.remove('a')
print(items)
  1. Predict the output of the following fragment, and give a one-line reason for the difference between the two printed lists. [3 marks]
nums = [10, 20, 30]
copy1 = nums
copy2 = nums[:]
nums.append(40)
print(copy1)
print(copy2)
  1. The following fragment does not run. Name the error it reports, explain in one line why it occurs, and rewrite the fragment correctly. [3 marks]
marks = [45, 62, 38, 71]
marks = marks.sort()
print(marks[0])
  1. State the output of the following fragment, and then say in one sentence what it computes for any list of numbers. [3 marks]
values = [7, 3, 9, 2, 8]
count = 0
for i in range(len(values)):
    if values[i] > values[count]:
        count = i
print(count, values[count])

9.15 Case-Based Questions

  1. A school library records the code of every book issued during a day, in the order in which it was issued. A code appears once for each issue, so a popular book appears more than once. At the end of one day the list holds: [4 marks]
issued = ['L-14', 'L-09', 'L-14', 'L-27', 'L-09', 'L-14']
a. Write a statement that displays how many times the book `'L-14'` was issued that day. [1]
b. Write a statement that displays the position at which `'L-27'` first appears in the list. [1]
c. A late entry for the book `'L-33'` has to be placed at the very beginning of the record, and one issue of `'L-09'` was recorded by mistake and must be removed. Write the statements that do both, and then display the corrected list. [2]
  1. A bus depot records the number of passengers carried by each of its three buses on each of the five working days of a week. The record is kept as a nested list with one inner list per bus, the days being in order within each inner list. [5 marks]
counts = [[48, 52, 45, 60, 55],
          [30, 28, 35, 33, 31],
          [72, 68, 75, 70, 66]]
a. Write a statement that displays the number of passengers carried by the third bus on the second day. [1]
b. Write the statements that display the total number of passengers carried by the first bus during the whole week. [2]
c. A fourth bus joins the depot, having carried 40, 44, 41, 39 and 43 passengers on the five days. Write the statement that adds this record to `counts` as a further row, and state what `len(counts)` will return afterwards. [2]

9.16 Programming Practice

  1. Write a complete program that reads a stated number of distinct integers into a list and displays the second largest of them. The program must display the list before giving the answer. Submit the program and an identifier description table. [3 marks]

  2. Write a function that receives a list and returns a new list holding only the elements that stand at even indices, leaving the original list untouched. Submit the function together with a few statements that call it and display both lists. [4 marks]

  3. A shop keeps the stock quantity of each item in a list, the position in the list identifying the item. Write a complete program that reads the quantities and a reorder level from the user, and displays the position of every item whose stock is at or below that level. If no item needs reordering, the program must say so instead of printing nothing. Submit the program and one sample run. [4 marks]

  4. Write a function that reverses the list passed to it in place, without using reverse(), without slicing, and without creating a second list. Write a program that reads a list of numbers, calls the function, and displays the list before and after the call. Submit the function, the program, an identifier description table, and one line stating what the function returns. [5 marks]