Lists

Published

2023-08-01

Lists

The list is one of the most widely used data structures in Python. One could not enumerate all possible applications of lists.1 Lists are ubiquitous, and you’ll see they come in handy!

What is a list?

A list is a mutable sequence of objects. That sounds like a mouthful, but it’s not that complicated. If something is mutable, that means that it can change (as opposed to immutable, which means it cannot). A sequence is an ordered collection—that is, each element in the collection has its own place in some ordering.

For example, we might represent customers queued up in a coffee shop with a list. The list can change—new people can get in the coffee shop queue, and the people at the front of the queue are served and they leave. So the queue at the coffee shop is mutable. It’s also ordered—each customer has a place in the queue, and we could assign a number to each position. This is known as an index.

How to write a list in Python

The syntax for writing a list in Python is simple: we include the objects we want in our list within square brackets. Here are some examples of lists:

coffee_shop_queue = ['Bob', 'Egbert', 'Jason', 'Lisa', 
                     'Jim', 'Jackie', 'Sami']
scores = [74, 82, 78, 99, 83, 91, 77, 98, 74, 87]

We separate elements in a list with commas.

Unlike many other languages, the elements of a Python list needn’t all be of the same type. So this is a perfectly valid list:

mixed = ['cheese', 0.1, 5, True]

There are other ways of creating lists in Python, but this will suffice for now.

At the Python shell, we can display a list by giving its name.

>>> mixed = ['cheese', 0.1, 5, True]
>>> mixed
['cheese', 0.1, 5, True]

The empty list

Is it possible for a list to have no elements? Yup, and we call that the empty list.

>>> aint_nothing_here = []
>>> aint_nothing_here
[]

Accessing individual elements in a list

As noted above, lists are ordered. This allows us to access individual elements within a list using an index. An index is just a number that corresponds to an element’s position within a list. The only twist is that in Python, and most programming languages, indices start with zero rather than one.2 So the first element in a list has index 0, the second has index 1, and so on. Given a list of n elements, the indices into the list will be integers in the interval [0, n-1].

A list and its indices

A list and its indices

In the figure (above) we depict a list of floats of size eleven—that is, there are eleven elements in the list. Indices are shown below the list, with each index value associated with a given element in the list. Notice that with a list of eleven elements, indices are integers in the interval [0, 10].

Let’s turn this into a concrete example:

>>> data = [4.2, 9.5, 1.1, 3.1, 2.9, 8.5, 7.2, 3.5, 1.4, 1.9, 3.3]

Now let’s access individual elements of the list. For this, we give the name of the list followed immediately by the index enclosed in brackets:

>>> data[0]
4.2

The element in the list data, at index 0, has a value of 4.2. We can access other elements similarly.

>>> data[1]
9.5
>>> data[9]
1.9

IndexError

Let’s say we have a list with n elements. What happens if we try to access a list using an index that doesn’t exist, say index n or index n + 1?

>>> foo = [2, 4, 6]
>>> foo[3]   # there is no element at index 3!!!
Traceback (most recent call last):
  File "/.../code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
IndexError: list index out of range

This IndexError message is telling us there is no element with index 3.

Changing the values of individual elements in a list

We can use the index to access individual elements in the list for modification as well (remember: lists are mutable).

Let’s say there was an error in data collection, and we wanted to change the value at index 7 from 3.5 to 6.1. To do this, we put the list and index on the left side of an assignment.

>>> data 
[4.2, 9.5, 1.1, 3.1, 2.9, 8.5, 7.2, 3.5, 1.4, 1.9, 3.3]
>>> data[7] = 6.1
>>> data 
[4.2, 9.5, 1.1, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]

Let’s do another: We’ll change the element at index 2 to 4.7.

>>> data[2] = 4.7
>>> data
[4.2, 9.5, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]

Some convenient built-in functions that work with lists

Python provides many tools and built-in functions that work with lists (and tuples, which we’ll see soon). Here are a few such built-in functions:

description constraint(s) if any example
sum() calculates sum of elements values must be numeric or Boolean * sum(data)
len() returns number of elements none len(data)
max() returns largest value can’t mix numerics and strings; max(data)
must be all numeric or all strings
min() returns smallest value can’t mix numerics and strings; min(data)
must be all numeric or all strings

* In the context of sum(), max(), and min(), Boolean True is treated as 1 and Boolean False is treated as 0.

Using our example data (from above):

>>> sum(data)
52.8
>>> len(data)
11
>>> max(data)
9.5
>>> min(data)
1.4

It seems natural at this point to ask, can I calculate the average (mean) of the values in a list? If the list contains only numeric values, the answer is “yes,” but Python doesn’t supply a built-in for this. However, the solution is straightforward.

>>> sum(data) / len(data)
4.8

… and there’s our mean!

Some convenient list methods

We’ve seen already that string objects have methods associated with them. For example, .upper(), .lower(), and .capitalize(). Recall that methods are just functions associated with objects of a given type, which operate on the object’s data (value or values).

Lists also have handy methods which operate on a list’s data. Here are a few:

description constraint(s) if any example
.sort() sorts list can’t mix strings data.sort()
and numerics
.append() appends an item to list none data.append(8)
.pop() “pops” the last element off list cannot pop from empty list data.pop()
and returns its value or
removes the element at index i must be valid index data.pop(2)
and returns its value

There are many others, but let’s start with these.

Appending an element to a list

To append an element to a list, we use the .append() method, where x is the element we wish to append.

>>> data
[4.2, 9.5, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]
>>> data.append(5.9)
>>> data 
[4.2, 9.5, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3, 5.9]

By using the .append() method, we’ve appended the value 5.9 to the end of the list.

“Popping” elements from a list

We can remove (pop) elements from a list using the .pop() method. If we call .pop() without an argument, Python will remove the last element in the list and return its value.

>>> data.pop()
5.9
>>> data
[4.2, 9.5, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]

Notice that the value 5.9 is returned, and that the last element in the list (5.9) has been removed.

Sometimes we wish to pop an element from a list that doesn’t happen to be the last element in the list. For this we can supply an index, .pop(i), where i is the index of the element we wish to pop.

>>> data.pop(1)
9.5
>>> data
[4.2, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]

For reasons which may be obvious, we cannot .pop() from an empty list, and we cannot .pop(i) if the index i does not exist.

Sorting a list in place

Now let’s look at .sort(). In place means that the list is modified right where it is, and there’s no list returned from .sort(). This means that calling .sort() alters the list!

>>> data
[4.2, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]
>>> data.sort()
>>> data
[1.4, 1.9, 2.9, 3.1, 3.3, 4.2, 4.7, 6.1, 7.2, 8.5]

This is unlike the string methods like .lower() which return an altered copy of the string. Why is this? Strings are immutable; lists are mutable.

Because .sort() sorts a list in place, it returns None. So don’t think you can work with the return value of .sort() because there isn’t any! Example:

>>> m = [5, 7, 1, 3, 8, 2]
>>> n = m.sort()
>>> n
>>> type(n)
<class 'NoneType'>

Some things you might not expect

Lists behave differently from many other objects when performing assignment. Let’s say you wanted to preserve your data “as-is” but also have a sorted version. You might think that this would do the trick.

>>> data = [4.2, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]
>>> copy_of_data = data   # naively thinking you're making a copy
>>> data.sort()
>>> data
[1.4, 1.9, 2.9, 3.1, 3.3, 4.2, 4.7, 6.1, 7.2, 8.5]

But now look what happens when we inspect copy_of_data.

>>> copy_of_data
[1.4, 1.9, 2.9, 3.1, 3.3, 4.2, 4.7, 6.1, 7.2, 8.5]

Wait! What? How did that happen?

When we made the assignment copy_of_data = data we assumed (quite reasonably) that we were making a copy of our data. It turns out this is not so. What we wound up with was two names for the same underlying data structure, data and copy_of_data. This is the way things work with mutable objects (like lists).3

So how do we get a copy of our list? One way is to use the .copy() method.4 This will return a copy of the list, so we have two different list instances.5

>>> data = [4.2, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]
>>> copy_of_data = data.copy()   # call the copy method 
>>> data.sort()
>>> data
[1.4, 1.9, 2.9, 3.1, 3.3, 4.2, 4.7, 6.1, 7.2, 8.5]
>>> copy_of_data
[4.2, 4.7, 3.1, 2.9, 8.5, 7.2, 6.1, 1.4, 1.9, 3.3]

A neat trick to get the last element of a list

Let’s say we have a list, and don’t know how many elements are in it. Let’s say we want the last element in the list. How might we go about it?

We could take a brute force approach. Say our list is called x.

>>> x[len(x) - 1]

Let’s unpack that. Within the brackets we have the expression len(x) - 1. len(x) returns the number of elements in the list, and then we subtract 1 to adjust for zero-indexing (if we have n elements in a list, the index of the last element is n - 1). So that works, but it’s a little clunky. Fortunately, Python allows us to get the last element of a list with an index of -1.

>>> x[-1]

You may think of this as counting backward through the indices of the list.

A puzzle (optional)

Say we have some list x (as above), and we’re intrigued by this idea of counting backward through a list, and we want to find an alternative way to access the first element of any list of any size with a negative-valued index. Is this possible? Can you write a solution that works for any list x?

Original author: Clayton Cafiero < [given name] DOT [surname] AT uvm DOT edu >

No generative AI was used in producing this material. This was written the old-fashioned way.

This material is for free use under either the GNU Free Documentation License or the Creative Commons Attribution-ShareAlike 3.0 United States License (take your pick).

Footnotes

  1. If you’ve programmed in another language before, you may have come to know similar data structures, for example, ArrayList in Java, mutable vectors in C++, etc. However, there are many differences, so keep that in mind.↩︎

  2. Some languages are one-indexed, meaning that their indices start at one, but these are in the minority. One-indexed languages include Cobol, Fortran, Julia, Matlab, R, and Lua.↩︎

  3. The reasons for this state of affairs is beyond the scope of this text. However, if you’re curious, see: https://docs.python.org/3/library/copy.html.↩︎

  4. There are other approaches to creating a copy of a list, specifically using the list constructor or slicing with [:], but we’ll leave these for another time. However, slicing is slower than the other two. Source: I timed it.↩︎

  5. Actually it makes what’s called a shallow copy. See: https://docs.python.org/3/library/copy.html.↩︎