3. Lists#

3.1. A list is a sequence#

A python list is a sequence of values which can be of any (and mixed) type. The values in a list are called elements or sometimes items. There are several ways to create a new list; the simplest is to enclose the elements in square brackets [ ].

[10, 20, 30, 40]
[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']
['crunchy frog', 'ram bladder', 'lark vomit']

The first example is a list of four integers. The second is a list of three strings. The elements of a list don’t have to be the same type. The following list contains a string, a float, an integer, and another list.

["hello",3.14,42,[True,2.71]]
['hello', 3.14, 42, [True, 2.71]]

A list within another list is often referred to as being nested list. Data structures, such as lists, which can contain their own type are called recursive data structures.

A list that contains no elements is called an empty list; you can create one with empty brackets, [ ].

[]
[]

As you might expect, you can assign list values to variables.

cheeses = ['Cheddar', 'Edam', 'Gouda']
numbers = [17, 123]
empty = []

3.2. Accessing and manipulating lists#

The syntax for accessing the elements of a list is the the bracket operator. The expression inside the brackets specifies the index which start at 0.

print(cheeses[0])
Cheddar

This behaviour is similar to that of Python strings. For example,

msg = "hello world"
msg[0]
msg[7]
'o'

When the bracket operator appears on the left side of an assignment, it identifies the element of the list that will be assigned.

numbers = [17, 123]
numbers[1] = 5
print(numbers)
[17, 5]

The one-th element of numbers, which used to be 123, is now 5. However, this is not the case for a string.

msg[2] = "a"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [9], line 1
----> 1 msg[2] = "a"

TypeError: 'str' object does not support item assignment

This reflects the fact that in Python lists are “mutable” (can be modified), and strings are “immutable” (cannot be modified). To modify an existing string you have to create a new one from the old using a function - this is not the case for lists.

3.2.1. Exercise 1#

Create a string and assign it to a variable. Now make a new string that is the concatenation of the original and assign it to a new variable.

s1 = "hello"
s2 = s1 + " world"
print(s2)
hello world

You can think of a list as a relationship between indices and elements. This relationship is called a mapping; each index “maps to” one of the elements.

title

Lists are represented by boxes with the word ``list’’ outside and the elements of the list inside. cheeses refers to a list with three elements indexed 0, 1 and 2. numbers contains two elements; the diagram shows that the value of the second element has been reassigned from 123 to 5. empty refers to a list with no elements.

List indices work the same way as string indices:

  • Any integer expression can be used as an index.

  • If you try to read or write an element that does not exist, you get an IndexError.

  • If an index has a negative value, it counts backward from the end of the list.

The in operator can be used to fond if an element is in a list.

cheeses = ['Cheddar', 'Edam', 'Gouda']
'Edam' in cheeses
True
'Brie' in cheeses
False

3.3. List operations#

The + operator concatenates lists.

a = [1, 2, 3]
b = [4, 5, 6]
c = a + b
print(c)
[1, 2, 3, 4, 5, 6]

3.3.1. Exercise 1#

Find out what the * operator does with lists.

3.3.2. Solution 1#

[0] * 4
[1, 2, 3] * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]

There is a “slice”” operator for lists.

t = ['a', 'b', 'c', 'd', 'e', 'f']
t[1:3]
['b', 'c']
t[:4]
['a', 'b', 'c', 'd']
t[3:]
['d', 'e', 'f']

If you omit the first index, the slice starts at the beginning. If you omit the second, the slice goes to the end. So if you omit both, the slice is a copy of the whole list.

t[:]
['a', 'b', 'c', 'd', 'e', 'f']

Since lists are mutable, it is often useful to make a copy before performing operations that fold, spindle or mutilate lists. A slice operator on the left side of an assignment can update multiple elements.

t[1:3] = ['x', 'y']
print(t)
['a', 'x', 'y', 'd', 'e', 'f']

You can add elements to a list by squeezing them into an empty slice.

t = ['a', 'd', 'e', 'f']
t[1:1] = ['b', 'c']
print(t)
['a', 'b', 'c', 'd', 'e', 'f']

And you can remove elements from a list by assigning the empty list to them.

t = ['a', 'b', 'c', 'd', 'e', 'f']
t[1:3] = []
print(t)
['a', 'd', 'e', 'f']

3.4. List methods#

Python provides methods that operate on lists. For example, append adds a new element to the end of a list.

t = ['a', 'b', 'c']
t.append('d')
print(t)
['a', 'b', 'c', 'd']

extend takes a list as an argument and appends all of the elements. The following example leaves t2 unmodified.

t1 = ['a', 'b', 'c']
t2 = ['d', 'e']
t1.extend(t2)
print(t2)
['d', 'e']

sort arranges the elements of the list from low to high.

t = ['d', 'c', 'e', 'b', 'a']
t.sort()
print(t)
['a', 'b', 'c', 'd', 'e']

List methods are all void; they modify the list and return None.

3.4.1. Exercise 2#

What does the follwing code do ?

t = t.sort()
print(t)
None

3.5. Aliasing#

If a refers to an object and you assign b = a, then both variables refer to the same object.

a = [1, 2, 3]
b = a
b is a
True

The state diagram for this is.

title

3.5.1. Exercise 3#

What does the python keyword is do ?

3.5.2. Exercise 4#

What do you think the output of the follwing code is ?

b = [1,2,3]
print(b == a)
print(b is a)
True
False

3.5.3. Exercise 5#

What would the state diagram look like now ?

The association of a variable with an object is called a reference.
In the state diagram, there are two references to the same object, in this case, a list. An object with more than one reference has more than one name, so we say that the object is aliased. If the aliased object is mutable, changes made with one alias affect the other:

a = [1,2,3]
b = a
b[0] = 17
print(a)
[17, 2, 3]

Although this behavior can be useful, it is error-prone. In general, it is safer to avoid aliasing when you are working with mutable objects. For immutable objects like strings, aliasing is not as much of a problem.

3.6. Functions and aliasing#

def delete_head(t):
    del t[0]
letters = ['a', 'b', 'c']
delete_head(letters)
print(letters)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_90992/2410107871.py in <module>
      1 letters = ['a', 'b', 'c']
----> 2 delete_head(letters)
      3 print(letters)

NameError: name 'delete_head' is not defined

The parameter t and the variable letters are aliases for the same object. This can be visualised using a stack diagram.

title

3.7. Pick an idiom and stick with it.#

Part of the problem with lists is that there are too many ways to do things. For example, to remove an element from a list, you can use pop, remove, del, or even a slice assignment.

3.8. Exercise 6#

What will the ouput of the following code be ?

t = [1,2,3,4]
x = 3
t.append(x)
print(t)
t = t + [x]
print(t)
[1, 2, 3, 4, 3]
[1, 2, 3, 4, 3, 3]

3.9. Exercise 7#

What will the ouput of the following code be ?

t = [1,2,3,4]
x = 3
t.append([x]) 
print(t)
t = t.append(x)
print(t)
t + [x]
print(t)
t = t + x
print(t)
[1, 2, 3, 4, [3]]
None
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-67-1668ad4e2719> in <module>
      5 t = t.append(x)
      6 print(t)
----> 7 t + [x]
      8 print(t)
      9 t = t + x

TypeError: unsupported operand type(s) for +: 'NoneType' and 'list'

After a while, you will probably find a way of working with lists that suits you best, but try and stick to just one way of doing a particular operation in your code. This will help with debugging and help communicate the intent of your code more effectively.