Lists

Lists provide a way to represent ordered sequences of data. They are an essential part of programming in Python and you will use them repeatedly in your work. The items in a list, also known as the elements of the list, do not need to have the same type (i.e., a single list can contain strings, floats, ints, and even other lists).

Literals

List literals are written using square brackets with the individual elements separated by commas. The empty list is written as []. Here are some examples taken from an ipython3 session:

In [1]: l0 = []

In [2]: l1 = [1, "abc", 5.7, [1, 3, 5]]

The variable l0 refers to an empty list. The variable l1 refers to a list with four elements: an integer, a string, a floating point number, and a list.

Length

You can find the length of a list using the built-in len function. The expression len(l0), for example, will evaluate to 0. Evaluating the expression len(l1) will yield the value 4.

Indexing

When used in an expression, the indexing operation ([]) is used to retrieve the value of the i-th element of a list. That is, given a list l, the expression l[i] will evaluate to the value of the i-th element of the list. Here are some example uses of the indexing operation:

In [3]: l1[0]
Out[3]: 1

In [4]: l1[1]
Out[4]: 'abc'

In [5]: l1[3]
Out[5]: [1, 3, 5]

Notice that we use zero to get the first element of the list and that the last element in a list, l, is at index len(l)-1. All indexing is zero-based in Python. In other words, in a list with five elements, the first element is at index 0, while the last element is at index 4. This characteristic of Python is important to note because some other programming languages, primarily older languages like Fortran, Matlab, and Smalltalk, use 1 to index the first element. As you learn other programming languages, you will find that a fair bit of your knowledge transfers over, but you must be cognizant of these sorts of distinctions.

Lists are mutable. That is, we can change them. To change the value at index i in list l, you simply put l[i] on the left-hand side of an assignment statement. For example:

In [6]: l1[2] = "xyz"

In [7]: l1
Out[7]: [1, 'abc', 'xyz', [1, 3, 5]]

Notice that the statement l1[2] = "xyz" changes the value at index 2 from 5.7 to be the string "xyz".

Debugging hint: Forgetting about zero-based indexing is the source of two kinds of problems in Python: (1) retrieving the second element when you want the first and (2) indexing beyond the end of the list in an attempt to get the last item in the list. The first of these problems can be difficult to track down, because your program will not fail at the point of the error. In fact, it may not fail at all! The second problem will cause your program to fail, which can be frustrating, but at least you know there is a problem!

While these examples all use integer literals (0, 1, etc.), any expression that yields an integer can be used to describe the index for a list. The expression is evaluated to yield an integer that is then used to index into the list. For example:

In [8]: i = 0

In [9]: l1[(i+1)*2]
Out[9]: "xyz"

In [10]: l2 = [1, 0, 3, 2]

In [11]: l1[l2[3]]
Out[11]: "xyz"

In [12]: l1[l2[3]] = 5.7

In  [13]: l1
Out [13]: [1, 'abc', 5.7, [1, 3, 5]]

The first example is straightforward. The second is a bit more complex: the expression l2[3] yields the value 2, which is then used to index into the list l1, which yields the string "xyz". The third statement changes the value of l1 at index 2 back to 5.7.

The result of indexing into a list could yield a value that itself may be a list, which can be indexed into like any other list:

In [14]: l1[3][0]
Out[14]: 1

Slicing

In addition to getting a single value from a list, it can be useful to extract a copy of a sub-list from a list. The slicing operation (:) is used for this purpose. The expression l1[1:3], for example, yields a new list with the value ['abc', 5.7]. Notice that the slice yields the elements from indices up to, but not including, the upper bound. To make a copy of a list l, of length N, you can use the expression l[0:N].

The expression on either side of the colon (e1 : e2) or even both expressions can be left out. The default value for the first expression (e1) is 0. The default value for the second expression is the length of the list. Here are some example uses of slicing:

In [15]: l1[1:3]
Out[15]: ['abc', 5.7]

In [16]: l1[:3]
Out[16]: [1, 'abc', 5.7]

In [17]: l1[1:]
Out[17]: ['abc', 5.7, [1, 3, 5]]

In [18]: l1[:]
Out[18]: [1, 'abc', 5.7, [1, 3, 5]]

Useful operations on lists

The equality operator (==) signifies value equality in Python. For lists, that means that two lists are deemed to be equal if they hold the same values in the same order. For example, evaluating the expression [1, 2, 3] == [1, 2, 3] will yield the value True, while evaluating the expression [1, 2, 3] == [] will yield the value False.

The append method is used to add a new element to the end of an existing list. It is important to note that this operation changes the value of the list. For example, after executing the statement l1.append(27), l1 will have the value [1, 'abc', 5.7, [1, 3, 5], 27]. You’ll notice that this statement uses somewhat unusual syntax: rather than looking like a function call, the name of the method (preceded by a dot) comes after the list! We will explain this notation in detail later in the term. For now, just remember that appending elements to a list changes the list and uses a somewhat odd notation.

The concatenation operation (+) creates a new list from two existing lists. For example:

In [19]: l3 = [1, 2, 3]

In [20]: l4 = ['a', 'b', 'c']

In [21]: l5 = l3 + l4

In [22]: l5
Out[22]: [1, 2, 3, 'a', 'b', 'c']

In [23]: l5[2] = 7

In [24]: l5
Out[24]: [1, 2, 7, 'a', 'b', 'c']

In [25]: l3
Out[25]: [1, 2, 3]

Notice that the update to l5 does not change the value of l3, because l5 is a new list that has copy of the values from l3 followed by a copy of the values from l4.

At times, you will want to create a list of a specific size with some initial value. You could do this task with a loop, but the concatenation mechanism provides a useful shorthand. Given a list l and a value n, the expression l*n concatenates n copies of the list l into a new list. (If this notation seems odd, recall that multiplication is just repeated addition.) For example, the expression [0]*10 creates a list of length 10 with of all zeros and the expression [False]*len(l7) creates a list that has the same length as l7 and in which every element has the value False. Notice that in this second example, the number of copies is determined by evaluating the expression len(l7).

The list initialization mechanism works well for simple values, such as integers, floats, and booleans, and for immutable values, such as strings, but it does not work well for more complex values, such as lists. The expression [[]]*5, for example, will yield a list with five elements, each of which refers to the same empty list! Here is some sample code to help make this issue more concrete:

In [26]: l = [[]]*5

In [27]: l[0].append("a")

In [28]: l
Out[28]: [['a'], ['a'], ['a'], ['a'], ['a']]

Why does this happen? Evaluating the inner expressions ([]) yields a reference to a newly created empty list. It is that reference that is copied five times when evaluating the full expression [[]]*5. As a result, updating one element of the l updates them all.