Sequences

Sequences are types of data, which consists of an ordered chain of elements. This means that certain elements of a sequence can be accessed with an index of type integer. The index is written in square brackets to distinguish it from the notation of calling a function.

Strings as Sequences

In Python strings are sequences! We may assign a string to a variable and then we can access the the individual characters of the string by their location relative to the start of the string.

word = 'Python'
print(word[0])
print(word[3])
P
h

We can also use negative numbers as an index. We then locate the elements relative to the end of the sequence.

print(word[-1])
print(word[-4])
n
t

Note that indexing starts with 0 in Python! For the string Python, the indices of the individual characters are given by

+---+---+---+---+---+---+
| P | y | t | h | o | n |
+---+---+---+---+---+---+
0   1   2   3   4   5   6
-6  -5  -4  -3  -2  -1

We can also assign all elements of a sequence directly by using a multiple assignment operation.

word2 = "abc"
c1, c2, c3 = word2
print(c1)
print(c2)
print(c3)
a
b
c

Here, the sequence word2 on the right side of the assignment operator is expanded into its elements before the assignment is done.

Sequences also support a limited set of arithmetics, that is a + and * operation. Summing two sequences means concatenating them.

word1 = "Foo"
word2 = "bar"
print(word1 + word2)
Foobar

A sequence can also be multiplied with an integer. The result is a sequence that repeats itself accordingly.

word1 = "bla"
print(3 * word1)
blablabla

There are more common operations on sequences, such as min(), max(), len() and in. Take a look at the documentation to familiarize yourself with them.

Slicing

Slicing creates a new sequence from another one. For this we use the start:stop:step notation when indexing the sequence. start is the index of the first element, stop the index of the last possible element of the slice and step is the distance between two selected elements.

To get the second to sixth character of the word Python, we do

word = 'Python'
print(word[1:5:1])
ytho

Remember that Python starts counting the elements at zero! If one of the parameters start, stop or step is omitted, the following default values are used: start=0, stop=end, step=1.

print(word[3:])
print(word[:3])
print(word[1:3])
print(word[::2])
print(word[::-1])
hon
Pyt
yt
Pto
nohtyP

Of course, we can also use negative indices here.

print(word[-2:])
on

The length of the stride is given by (stop - start) // step. Hence, if stop equals start, we get a sequence of length zero, i.e. it does not contain any element.

word[2:2]  # This is a empty string
''

Tuples

Tuples are ordered sequences of arbitrary data objects. A tuple is build with a comma separated list of expressions which can be set in round brackets:

t = 1, 23.45, "some text"
print(t)

t2 = (1, 23.45, "some text")
print(t == t2)
(1, 23.45, 'some text')
True

Tuples can have any object as an element, so also tuples can be an element of a tuple.

t2 = 56., 1, (45, 7, 78, 3)
print(t2)
(56.0, 1, (45, 7, 78, 3))

Since tuples are sequences, elements of tuples can be accessed using a integer index in the same way as we have seen for strings.

print(t[0])
print(t[-1])
1
some text

And, of course, also slicing and sequence arithmetics are possible.

print(3 * t[:2])
(1, 23.45, 1, 23.45, 1, 23.45)

Lists

Lists are sequences of arbitrary objects, similar to tuples. Lists are always set in square brackets.

squares = [1, 4, 9, 16, 25]
print(squares[0])
print(squares[-3:])
print(squares + [36, 49])
1
[9, 16, 25]
[1, 4, 9, 16, 25, 36, 49]

The difference to tuples is, that lists are changeable. We say a list is mutable. As a consequence, we can replace, add or remove elements of an existing list without the need to create a new list.

cubes = [1, 8, 27, 65, 125]
print(cubes)

# replace the fourth element of the list
cubes[3] = 4**3
print(cubes)
[1, 8, 27, 65, 125]
[1, 8, 27, 64, 125]
letters = ['a','b','c','d','e','f','g']

# replace multiple values at the same time
letters[2:5] = ['C','D','E']
print(letters)
['a', 'b', 'C', 'D', 'E', 'f', 'g']
# remove elements from the list
letters[2:5] = []
print(letters)
['a', 'b', 'f', 'g']
# remove all elements
letters[:] = []
print(letters)
[]

Lists offer methods to manipulate their content:

Method

Description

list.append(x)

Appends element x at the end of a list

list.extend(l)

Appends list l to another list

list.insert(i,x)

Inserts element x at index i

list.remove(x)

Removes the first element with value x

list.index(x)

Index of the first element with value x

list.count(x)

Counts elements with value x

list.sort(...)

Sorts the list (in place), available arguments change the order (read the docs)

list.reverse()

Reverses the order of elements (in place)

list.pop([i])

Removes and returns the element at index i

Here are some examples of applying list methods:

stuff = [1, 'ham', 'eggs', 42.]

# add a single element
stuff.append('spam')
print(stuff)
[1, 'ham', 'eggs', 42.0, 'spam']
# add several eleme
stuff.extend(['a', 'b', 'c'])
print(stuff)
[1, 'ham', 'eggs', 42.0, 'spam', 'a', 'b', 'c']

List comprehension

A list comprehension is a shortened way of creating a list. It is particularly useful if we want to apply some operation to all elements of a sequence to create another one.

a = [x ** 2 for x in (1, 2, 3, 4, 5)]
print(a)
[1, 4, 9, 16, 25]

It is also possible to create multidimensional list comprehension, that is we apply some operation to all combination of elements of several sequences. In the example below, we use the function range() to create sequences of integers. Take a look at its’ documentation, it is a very useful function.

a = [x + y for x in range(3)
     for y in range(3)]
print(a)
[0, 1, 2, 1, 2, 3, 2, 3, 4]

List comprehension can also be conditional in two ways. We can either perform different operations on the elements of the sequence, depending on some condition, or we can exclude some elements from being processed.

# performing different operations
a = [x ** 2 if x % 2 == 0 else
     x ** 3 if x % 3 == 0 else
     x for x in range(10)]
print(a)
[0, 1, 4, 27, 16, 5, 36, 7, 64, 729]
# excluding elements
a = [x ** 2 for x in range(10) if x % 2 == 0]
print(a)
[0, 4, 16, 36, 64]

Note that in the second example, the resulting list has less than ten elements, since all odd elements are excluded.

While list comprehension are useful shortcuts, too complicated comprehensions might obfuscate your code. In this case, consider using a loop to create the list. How to do that will be discussed in a later chapter. Remember: it is always better to write more code that is better readable then less code that is less readable.

Dictionaries

Dictionaries are unsorted, mutable collections of objects, which can be indexed with a key. A key can be any object of immutable type, typically strings. Dictionaries can be created using curly brackets or the function dict():

# Create dictionary
tel = {'jack': 4098, 'sape': 4139}

# this is equivalent
tel = dict(jack=4098, sape=4139)

For dictionaries, we call the index key and the corresponding element value. To access a value, we use the corresponding key as the index.

print(tel['jack'])
4098

Since dictionaries are mutable, we can change the values of the dictionary or add and remove some. To add a value we simply assign some data to some new key.

# add a value
tel['guido'] = 4127
print(tel)
{'jack': 4098, 'sape': 4139, 'guido': 4127}

To delete a key-value pair, we use the del statement as below.

# Delete key
del tel['sape']
print(tel)
{'jack': 4098, 'guido': 4127}

We may also check if some key exist in our dictionary.

# check for existence of key
print('guido' in tel)
True

Dictionaries offer methods to work with them:

Method

Description

d.clear()

Deletes all elements

d.copy()

Returns a copy of the dictionary

d.items()

Returns a list with (key, value) pairs

d.keys()

Returns a list of keys

d.pop(key)

Deletes and returns the value of a key

d.update(...)

Inserts new elements form another dictionary or key-value pair.

Here are some examples:

d = dict(a=1, b=2)
print(d.items())
print(d.keys())
dict_items([('a', 1), ('b', 2)])
dict_keys(['a', 'b'])
# create a copy
d1 = d.copy()
print(d1 == d)
print(d1 is d)
True
False
# delete everything
d.clear()
print(d)
{}
# Pop a value from the dictionary
print(d1.pop('a'))
print(d1)
1
{'b': 2}

Mutable and Immutable: reference or copy?

All types in Python are either mutable or immutable. While the distinction may not seem to be very important, it causes a fundamentally different behavior when assigning to a variable. Assignments to immutable types always creates a copy of the object. Assigning to a mutable type creates a reference to the data. To understand what is meant by reference, consider the following code example:

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

We assign the name b to the data associated with variable a, that is the list [1, 2, 3]. A list is mutable. Hence, in the assignment, we do not create a second list in the memory of the computer with the same content that a and call it b. We rather instruct the computer that b is pointing to the same location in memory, hence the identical data, than a. If we change b, we also change the content of a.

We can actually check if two variables point to the same data in memory (are identical) by using the is operator.

print(a is b)
True

Sometimes, we need to create a copy instead of a reference. For example, if we want to manipulate the content of a dictionary while keeping a unchanged version. Most mutable types offer a copy method to create a copy.

a = {'a': 1}

# this is not a copy
b = a
print(a is b)

# this is a copy
c = a.copy()
print(a is c)
True
False

If we slice a list, we will also get a copy of the values.

list_1 = [1, 2, 3]
list_2 = list_1[:]
print(list_1 is list_2)
False