Chapter 5. Iterables#
An iterable is a data structure that allows you to store multiple items in a single variable. Put more formally, an iterable object is capable of returning its members one after another.
You already saw an important iterable - the string. There are many more iterables in Python - but in this chapter we will only concern ourselves with the most important ones - lists, ranges, tuples and dictionaries. The first three (i.e. lists, ranges and tuples) are special iterables called sequences. Sequences are iterables which support access using a numerical index (we will see what this means in a second).
Lists#
Probably the most important iterable in Python is the list. Lists are mutable sequences, i.e. sequences whose contents can be changed. You can create a list using square brackets []
. Individual list items have to be separated with commas:
squares = [0, 1, 4, 9, 16]
squares
[0, 1, 4, 9, 16]
type(squares)
list
Every item in a list can be accessed by its index:
Note that indices start with 0
and not with 1
. Therefore, if you want to access the first item of a list, you need to access the item with index 0
. This means that in the case of the squares
list the item with index 1
is 1
, the item with index 2
is 4
, the item with index 3
is 9
etc.
You can access an item of a list by its index using the square bracket notation. For example, this how you would access the item of the squares
list at index 2
:
squares[2]
4
Let’s access every item in the squares
list using its index:
squares[0]
0
squares[1]
1
squares[2]
4
squares[3]
9
squares[4]
16
What happens if we try to access the item by an index that doesn’t exist? We get an IndexError
:
squares[5]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[10], line 1
----> 1 squares[5]
IndexError: list index out of range
Generally speaking, the indices of the list range from 0
until the length of the list minus 1.
Speaking of the length of the list - we can get it using the len
function:
len(squares)
5
We can also obtain multiple items of a list using the so-called slice notation. Instead of specifying a single index, you can specify a start and stop index separated by the colon symbol :
. This returns all the items from index start
to the index stop - 1
(i.e. the stop
index is excluded).
For example, if we want to get a list containing the items of the squares
list at the indices 2
, 3
and 4
we would specify a slice with start
being 2
and stop
being 5
.
squares[2:5]
[4, 9, 16]
The reason stop
is excluded is because the length of the returned list should be equal to stop - start
:
start = 2
stop = 5
len(squares[start:stop]) == stop - start
True
If we specify a stop
index that is outside the list, slicing still works. All indices beyond the stop
index are simply ignored:
squares[2:27]
[4, 9, 16]
We can omit the start
index if we want to start at the beginning of the list:
squares[:3]
[0, 1, 4]
We can omit the stop
index as well if we want to get all elements until the end of the list:
squares[3:]
[9, 16]
Remember how we said that lists are mutable? Using the square bracket notation we can not just access the item of a list, but also change it.
For example this is how we could change the item at index 2
to have a value of 5
:
squares[2] = 5
This is how the new list looks like:
squares
[0, 1, 5, 9, 16]
Since the square of 2
is 4
and not 5
let us quickly change it back before anyone notices:
squares[2] = 4
squares
[0, 1, 4, 9, 16]
Lists are objects and therefore have a whole bunch of useful methods. For example we can append a value to the end of a list using the appropriately named append
method:
squares.append(25)
squares
[0, 1, 4, 9, 16, 25]
We can insert a value at any place in the list using the insert
method. This method takes the index to insert the value at and the value to be inserted.
Here is how we would insert the the value 7
at index 3
of the squares
list:
squares.insert(3, 7)
squares
[0, 1, 4, 7, 9, 16, 25]
Finally, you can delete an index using the pop
method. This method also returns the deleted item:
value = squares.pop(3)
squares
[0, 1, 4, 9, 16, 25]
value
7
These methods allow us to create lists, update items in lists, insert items in lists and remove items from lists.
Additionally, we can concatenate lists using the +
operator:
[1, 2, 3] + [4, 5, 6]
[1, 2, 3, 4, 5, 6]
We can also extend lists in place using the +=
operator or the extend
method:
list1 = [1, 2, 3]
list1 += [4, 5, 6]
list1
[1, 2, 3, 4, 5, 6]
list2 = [1, 2, 3]
list2.extend([4, 5, 6])
list2
[1, 2, 3, 4, 5, 6]
Ranges#
A range represents an immutable (i.e. unchangeable) sequence of numbers. You can construct a range using the built-in range
function. This takes a start
argument and an stop
argument which indicate the boundaries of the range:
my_range = range(0, 4)
type(my_range)
range
my_range
range(0, 4)
my_range.start
0
my_range.stop
4
We can convert a range to a list using the built-in list function:
list(range(0, 4))
[0, 1, 2, 3]
Note that the range goes from start
to stop - 1
(this is similar to slices). Therefore in this case the number 4
is not part of the resulting list.
The reasoning behind this is exactly the same as with slices. We want my_range.stop - my_range.start
to be equal to the length of my_range
:
len(my_range) == my_range.stop - my_range.start
True
It is also possible to call range
with a single value, in which case that value is interpreted as the stop
value and the start
value is set to 0:
range(5)
range(0, 5)
list(range(5))
[0, 1, 2, 3, 4]
Tuples#
Tuples are immutable sequences. We can construct tuples in a similar way as lists, just without the square brackets []
:
color = 0, 127, 0
type(color)
tuple
Unlike ranges, tuples don’t need to consist of numbers and generally don’t need to hold values of the same data type:
my_tuple = 42, "flunky"
my_tuple
(42, 'flunky')
Note that technically lists can also hold values of different data types, however usually lists are used to hold values of the same data type.
We should point out that we can add parentheses ()
around the tuple items for better readability:
my_tuple = (42, "flunky")
my_tuple
(42, 'flunky')
Sometimes the parentheses are necessary, for example when we try to create an empty tuple:
empty_tuple = ()
type(empty_tuple)
tuple
empty_tuple
()
The parentheses are also necessary if we want to pass a tuple as an argument to a function:
def print_tuple(my_tuple):
print(my_tuple)
print_tuple((3, 2))
(3, 2)
If we try to leave the parentheses out, Python will think that we are passing two arguments to the function and yell at us:
print_tuple(3, 2)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[53], line 1
----> 1 print_tuple(3, 2)
TypeError: print_tuple() takes 1 positional argument but 2 were given
Just like with lists, we can access tuple elements by index:
color[1]
127
We can also get the length of a tuple using the len
function:
len(color)
3
However because tuples are immutable, we cannot change their values:
color[1] = 100
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[56], line 1
----> 1 color[1] = 100
TypeError: 'tuple' object does not support item assignment
While tuples are useful, they are often overused. A very tempting thing to do is to store data in tuples instead of objects. For example, you could do this:
ball = 10, 8
Here the first element of the tuple (ball[0]
) represents the position of the ball. The second element of the tuple (ball[1]
) represents the speed of the ball:
# Get the position of the ball
ball[0]
10
# Get the speed of the ball
ball[1]
8
This is a bad idea. The reason for that is quite simple - representing complex data structures with tuples makes our code less readable. It is totally unclear what ball[0]
is supposed to be if we just look at the respective line and don’t know the structure of the tuple. However the meaning of ball.pos
is totally clear even if we have no idea what else a ball can do - ball.pos
gives us the position of the ball.
Generally speaking you should only use tuples for very simple data which contains values that always belong together. Standard examples for valid tuple uses are things like RGB colors, video resolutions and positions:
rgb_color = 0, 127, 0 # the color green
video_resolution = 1280, 720 # the 720p video resolution
position = 24, 25 # a position in 2D
Remember how we returned multiple values from functions in chapter 3? Well, that was a lie. In reality we returned a single value that happened to be a tuple:
def get_color():
return 0, 127, 0
my_color = get_color()
my_color
(0, 127, 0)
type(my_color)
tuple
However because we can unpack tuples, we were able to pretend that we returned multiple values from a function:
r, g, b = my_color
r
0
g
127
b
0
This is what allowed us to write things like:
r, g, b = get_color()
Dictionaries#
Dictionaries are data structures that map (immutable) keys to values.
We can define a dictionary using the {}
bracket notation. The key-value pairs must be separated with commas. For example, here is how we could define a dictionary which maps countries to their capitals:
capitals = {"Germany": "Berlin", "France": "Paris", "Spain": "Madrid"}
capitals
{'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid'}
Another example is a dictionary which maps players to their ratings:
ratings = {"Alex": 1500, "Michael": 1400, "John": 1000, "Max": 1200}
Unlike with lists, we don’t access dictionary values by indexes. Instead we access the values by their keys.
For example if we want to access the capital of Germany, we provide the key "Germany"
to our dictionary:
capitals["Germany"]
'Berlin'
We could access the rating of "John"
in a similar manner:
ratings["John"]
1000
Just like with lists, we can add values to a dictionary. To do that, we need to specify both the key and the value of the new key-value pair:
capitals["Italy"] = "Rome"
capitals
{'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid', 'Italy': 'Rome'}
We can change the value of an existing key using the same notation. This will overwrite the current value for that key:
capitals["Germany"] = "Munich"
capitals["Germany"]
'Munich'
Let’s quickly change it back before Berlin gets mad:
capitals["Germany"] = "Berlin"
We can delete a value by its key using the pop
method. This method will also return the deleted value:
value = capitals.pop("Italy")
capitals
{'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid'}
value
'Rome'
Dictionaries are useful if we need to store values and access them using other values. Examples of good uses of dictionaries include ratings of players, capitals of countries, ages of people etc.