# Lists#

## Questions:#

• How can I store multiple values in a single variable?

## Learning Objectives:#

• Explain why programs need collections of values

• Write programs that create lists, index them, slice them, and modify them through assignment and method calls

## A list stores many values in a single structure#

• We have seen how to store values by assigning them to variables

• With data sets of any reasonable size, it would rapidly become inefficient and confusing to store each data point with a separate variable name

• For example, here are average life expectancies for Canada over 100 years, from the Gapminder data set:

life_exp_1900 = 48.1
life_exp_1920 = 56.6
life_exp_1940 = 64.0
life_exp_1960 = 71.0
life_exp_1980 = 75.2
life_exp_2000 = 79.2
• While you can store the data this way, it requires a lot of variable names. As well, although there is structure in the data (they reflect an ordered sequence of dates), this ordering is not reflected in how the data are stored.

• We can use a list to store many values together

• Lists are defined by one or more items contained within square brackets [], with individual values (list items) separated by commas ,:

life_exp = [48.1, 56.6, 64.0, 71.0, 75.2, 79.2]
print('Life expectancies:', life_exp)
Life expectancies: [48.1, 56.6, 64.0, 71.0, 75.2, 79.2]

Like strings, we can use the len() function to get the length of a list

print('length:', len(life_exp))
length: 6

Here, length refers to the number of items in the list

## Lists may contain values of different types#

A single list may contain virtually any Python data type, including integers, floats, strings, and even other lists!

personal_info = ['First Name', 'Costanza',
'Last Name', 'Raghnall',
'Age', 42,
'Height', 1.65,
'Children', ['Apoorva', 'Columbanus']
]

that Python allows us to input a list over multiple lines. Jupyter nicely indents the additional lines to help us see that they are continuations of the same list.

(Data generated by https://www.behindthename.com)

## Indexing is similar for lists and strings#

We previously saw how we could get single characters from a character string, using indexes in square brackets:

element = 'carbon'
print('zeroth character:', element[0])
print('third character:', element[3])
zeroth character: c
third character: b

### Use an item’s index to fetch it from a list#

Indexing works for lists as it does for strings:

print('zeroth item of life_exp:', life_exp[0])
print('fourth item of life_exp:', life_exp[4])
zeroth item of life_exp: 48.1
fourth item of life_exp: 75.2

## Slicing is similar for lists and strings#

Recall that we can slice a string to get a range of values, e.g.,

print('First three items:', element[0:3])

We can do the same thing with lists:

print('First three items in life_exp:', life_exp[0:4])
print('Last item in life_exp:', life_exp[-1])
First three items in life_exp: [48.1, 56.6, 64.0, 71.0]
Last item in life_exp: 79.2

## Indexing can be used to replace a value in a list#

life_exp[0] = 48.3
print(life_exp)
[48.3, 56.6, 64.0, 71.0, 75.2, 79.2]

## Appending items to a list lengthens it#

We can use the .append() method to add an item to the end of a list

Here we will add the life expectancy for 2020 in Canada to extend our list:

print(life_exp)
life_exp.append(82.3)
print(life_exp)
[48.3, 56.6, 64.0, 71.0, 75.2, 79.2]
[48.3, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3]

## Methods#

• .append() is a list method

• Methods are like functions, but tied to a particular type of object. In other words, the fact that there is a list .append() method does not mean that we can use .append() with other Python objects that aren’t lists.

• Use object_name.method_name() to call methods.

• We will meet other methods of lists as we go along.

• Use help(list) for a preview of the methods available for objects of type list

help(list)
Help on class list in module builtins:

class list(object)
|  list(iterable=(), /)
|
|  Built-in mutable sequence.
|
|  If no argument is given, the constructor creates a new empty list.
|  The argument must be an iterable if specified.
|
|  Methods defined here:
|
|      Return self+value.
|
|  __contains__(self, key, /)
|      Return key in self.
|
|  __delitem__(self, key, /)
|      Delete self[key].
|
|  __eq__(self, value, /)
|      Return self==value.
|
|  __ge__(self, value, /)
|      Return self>=value.
|
|  __getattribute__(self, name, /)
|      Return getattr(self, name).
|
|  __getitem__(...)
|      x.__getitem__(y) <==> x[y]
|
|  __gt__(self, value, /)
|      Return self>value.
|
|      Implement self+=value.
|
|  __imul__(self, value, /)
|      Implement self*=value.
|
|  __init__(self, /, *args, **kwargs)
|      Initialize self.  See help(type(self)) for accurate signature.
|
|  __iter__(self, /)
|      Implement iter(self).
|
|  __le__(self, value, /)
|      Return self<=value.
|
|  __len__(self, /)
|      Return len(self).
|
|  __lt__(self, value, /)
|      Return self<value.
|
|  __mul__(self, value, /)
|      Return self*value.
|
|  __ne__(self, value, /)
|      Return self!=value.
|
|  __repr__(self, /)
|      Return repr(self).
|
|  __reversed__(self, /)
|      Return a reverse iterator over the list.
|
|  __rmul__(self, value, /)
|      Return value*self.
|
|  __setitem__(self, key, value, /)
|      Set self[key] to value.
|
|  __sizeof__(self, /)
|      Return the size of the list in memory, in bytes.
|
|  append(self, object, /)
|      Append object to the end of the list.
|
|  clear(self, /)
|      Remove all items from list.
|
|  copy(self, /)
|      Return a shallow copy of the list.
|
|  count(self, value, /)
|      Return number of occurrences of value.
|
|  extend(self, iterable, /)
|      Extend list by appending elements from the iterable.
|
|  index(self, value, start=0, stop=9223372036854775807, /)
|      Return first index of value.
|
|      Raises ValueError if the value is not present.
|
|  insert(self, index, object, /)
|      Insert object before index.
|
|  pop(self, index=-1, /)
|      Remove and return item at index (default last).
|
|      Raises IndexError if list is empty or index is out of range.
|
|  remove(self, value, /)
|      Remove first occurrence of value.
|
|      Raises ValueError if the value is not present.
|
|  reverse(self, /)
|      Reverse *IN PLACE*.
|
|  sort(self, /, *, key=None, reverse=False)
|      Sort the list in ascending order and return None.
|
|      The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
|      order of two equal elements is maintained).
|
|      If a key function is given, apply it once to each list item and sort them,
|      ascending or descending, according to their function values.
|
|      The reverse flag can be set to sort in descending order.
|
|  ----------------------------------------------------------------------
|  Class methods defined here:
|
|  __class_getitem__(...) from builtins.type
|      See PEP 585
|
|  ----------------------------------------------------------------------
|  Static methods defined here:
|
|  __new__(*args, **kwargs) from builtins.type
|      Create and return a new object.  See help(type) for accurate signature.
|
|  ----------------------------------------------------------------------
|  Data and other attributes defined here:
|
|  __hash__ = None

## Many methods modify a data object in place#

Note in the above example of .append() that we didn’t need an assignment statement (i.e., a variable name and the = operator) to modify life_exp. That is, we didn’t write:

life_exp = life_exp.append(82.3)

We wrote:

life_exp.append(82.3)

In fact, the list .append() method modifies the list in place and returns None, so if we tried:

life_exp = life_exp.append(82.3)
print(life_exp)

The resulting value of life_exp would be:

None

which is definitely not what we want!

## Extending a list#

• the .append() method only allows you to add a single item to the end of a list

• see what happens if you try to append a list to an existing list:

life_exp_2000s = [85.1, 87.3, 89.5, 91.6]
life_exp.append(life_exp_2000s)
print(life_exp)
[48.3, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3, [85.1, 87.3, 89.5, 91.6]]

The result is what we call a nested list, where the appended list is stored as a single list item — of type list — in the original list. This is a consequence of the fact that lists can contain multiple types.

In this case, what we want is a flat list, a single list of floats.

The .extend() method will do this. It is similar to append, but it allows you to combine two lists. For example:

life_exp = [48.1, 56.6, 64.0, 71.0, 75.2, 79.2]
life_exp_2000s = [82.3, 85.1, 87.3, 89.5, 91.6]

life_exp.extend(life_exp_2000s)
print(life_exp)
[48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3, 85.1, 87.3, 89.5, 91.6]

## Insert a new list entry at a specific position#

The result of help(list) above includes a method .insert(), which takes two arguments: a value to insert, and the index (position) in the list that you want the element inserted before. For example, to add the value 46.9 to the start of our life_exp list, we would use:

life_exp.insert(0, 46.9)
print(life_exp)

## Removing items from a list#

There are two list methods that you can use to remove an element from a list:

• .remove() will remove the first instance of a specific value you give it as an argument

• .pop() will remove an element at a specific position (index) that you give as an argument

In the example below, we extend one list with another, but then we notice that the last value in the first list, is duplicated as the first value in the second list. As well, the last value in the second list is duplicated. So we need to remove one copy of each repeated value.

life_exp = [48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3]
life_exp_2000s = [82.3, 85.1, 87.3, 89.5, 91.6, 91.6]
life_exp.extend(life_exp_2000s)
print('With duplicated values:', life_exp)
With duplicated values: [48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3, 82.3, 85.1, 87.3, 89.5, 91.6, 91.6]

We will remove the value that’s duplicated in the two lists, 82.3, using .remove():

life_exp.remove(82.3)
print('Removed duplicate value:', life_exp)
Removed duplicate value: [48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3, 85.1, 87.3, 89.5, 91.6, 91.6]

We will remove the value that’s duplicated at the end using .pop():

life_exp.pop(-1)
print('Removed duplicate value:', life_exp)
Removed duplicate value: [48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3, 85.1, 87.3, 89.5, 91.6]

## An empty list contains no values#

We can assign [] to a variable name, to represent a list that doesn’t contain any values.

This can be a good starting point if we want to create a list by appending values as we calculate them

The example below is trivial, but in later lessons we will see better uses for empty lists

x = []
y = 1
x.append(y)
x.append(y + 7)
print(x)
[1, 8]

## Difference between strings and lists#

• Both strings and lists are called collections in Python, so they can be indexed

• Although indexing works similarly for string and lists, the two data types are different in other ways

• Importantly, strings are immutable — they cannot be changed after they have been created

• Python considers the string to be a single value with parts, not a collection of values

• In contrast, lists are mutable — as we saw above, they can be changed such as by:

• using indexing to change a single list element

• appending items to a list with .append() or .extend()

• deleting items from a list with del

So because we defined element above as a string, we cannot change one letter in the string:

element[0] = 'C'
element[0] = 'C'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[16], line 1
----> 1 element[0] = 'C'

TypeError: 'str' object does not support item assignment

Likewise, you cannot .append() to a string, nor can you del a string element.

## Indexing beyond the end of the collection is an error.#

Python reports an IndexError if we attempt to access a value that doesn’t exist.

• This is a kind of runtime error, as discussed in the section on built-ins

• It’s not a syntax error, because it cannot be detected as the code is parsed before being run; this is because the index might be calculated based on data

print('99th element of life_exp is:', life_exp[99])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[17], line 1
----> 1 print('99th element of life_exp is:', life_exp[99])

IndexError: list index out of range

## Fill in the Blanks#

Fill in the blanks so that the program below produces the output shown.

values = ____
values.____(1)
values.____(3)
values.____(5)
print('first time:', values)
values = values[____]
print('second time:', values)

Output:

first time: [1, 3, 5]

second time: [3, 5]

## How Large is a Slice?#

Imagine we have a list like life_exp above. If a and b are both non-negative integers (e.g., 2 and 5), how long is the list life_exp[a:b]?

## From Strings to Lists and Back#

What does the list() function do when applied to a string?

For example:

list('tin')

And what does the .join() method do?

E.g.:

''.join(['g', 'o', 'l', 'd'])

What happens if you put a character inside the quote marks for .join(), like this?

'-'.join(['g', 'o', 'l', 'd'])

## Indexing from the end#

What does the following program print?

life_exp = [48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3]
print(life_exp[-1])
82.3
1. How does Python interpret a negative index?

2. If a list or string has N elements, what is the most negative index that can safely be used with it, and what location does that index represent?

3. What does del life_exp[-1] do?

4. How can you display all elements but the last one in a list (or string)? (Hint: you will need to combine slicing and negative indexing.)

1. If we write a slice as start:stop:step, what does step do?

2. What expression would select all of the even-numbered items from a collection?

## Slice Bounds#

What does the following program print?

life_exp = [48.1, 56.6, 64.0, 71.0, 75.2, 79.2, 82.3]
print(life_exp[0:20])
print(life_exp[-1:3])

Even though we saw above that we get an error if we try to index a single element that is. outside the range of items in the list (e.g., print(life_exp[20]) would fail), we can use a value outside of the elements in a list when specifying a range.

# Program B
letters = list('gold')
letters_s = letters.sort()
print('letters is', letters, 'and letters_s is', letters_s)

## Copying (or Not)#

What do these two programs print? In simple terms, explain the difference between x = y and x = y[:].

# Program A
y = list('gold')
x = y
x[0] = 'D'
print('x is', x, 'and y is', y)
# Program B
y = list('gold')
x = y[:]
x[0] = 'D'
print('x is', x, 'and y is', y)

## Summary of Key Points:#

• A list stores many values in a single structure

• Lists may contain values of different types

• The empty list contains no values

• Use an item’s index to fetch it from a list

• Lists’ values can be replaced by assigning to them

• Appending items to a list lengthens it

• if .append() is used to add a collection of items (such as a list), they are added as a single new list item

• The list method .extend() can be used with a collection of items (such as another list) to create a flat list

• The list methods .remove() and .pop() can be used to delete list items, using either their value or index, respectively

• Character strings can be indexed like lists

• Character strings are immutable, whereas lists are mutable (changable)

• Indexing beyond the end of the collection is an error, but slicing can include indexes that do not occur in the list

This lesson is adapted from the Software Carpentry Plotting and Programming in Python workshop.