Sequence data types#

In this exercise, we explore the different types of sequence data types in Python. These are very important in Python, and provide a convenient way to deal with data of arbitrary sizes. First, we present immutable data types, these can not be changed after they are created, and then the mutable data types are shown. There are exercises throughout, and solutions are available at the end of the document.

Strings#

Strings are a simple case of a sequence data type as each character can be accessed using its index. The syntax for doing so uses square brackets.

a = "Hello"
print(a[0])  # indices start at 0, so this prints the first letter of the string
H

It is possible to select parts of the string using what is called slicing, selecting a range of indices. The begin and end indices are separated by a colon.

print(a[1:3])  # from 1 to (but not including) 3
print(a[1:])  # from 1 to (and including) end
print(a[:3])  # from start to (and not including) 3
print(a[2:-2])  # from 2 to (and not including) 2 from the end
el
ello
Hel
l

Strings can be concatenated using the + operator.

print("Hello" + " " + "World")
Hello World

Strings can be repeated by multiplying with an integer.

print(20 * "-o-")
-o--o--o--o--o--o--o--o--o--o--o--o--o--o--o--o--o--o--o--o-

It is possible to loop over the characters in a string with a for-loop.

for letter in a:
    print(letter)
H
e
l
l
o

There is a built-in len function that returns the length of any sequence data type, here for a string.

print(len(a))
5

Strings are implemented as objects, and these can have helpful methods associated with them. Here we show a few examples.

print(a)  # Original string
print(a.lower())  # Returns a as lower case letters
print(a.upper())  # Returns a as upper case letters
print(a.count("l"))  # Counts the number of occurrences of given string
Hello
hello
HELLO
2

Strings are immutable data structures, which means that the content can not be changed directly.

print(a)
# a[2] = "j" # uncomment this line to try and change an l to a j in Hello.
print(a)
Hello
Hello

(1) Task: Use slicing to print the ESS part of these strings.

A = "Professionalism"
B = "Multiprocessing"
C = "Brightness"
D = "Impressionistic"

Solution:

Hide code cell content
print(A[4:7])
print(B[9:12])
print(C[-3:])
print(D[4:7])
ess
ess
ess
ess

Tuples#

Tuples hold a sequence of objects that can be accessed by index. They are similar to strings, but instead of only being able to hold characters in a sequence, they can hold anything. It is important not to think of a Python tuple as a vector or other similar mathematical concept, tuples are just a container using indices to retrieve the information. Tuples are defined using parenthesis, and like strings are immutable.

tup = (1, 2.0, "three")
print(tup)
(1, 2.0, 'three')

The different elements of a tuple can be accessed in the same manner as a string, there is, however, an important difference. If a colon is not used, the return value is what the tuple contains at that index, but when a colon is used, a tuple containing the selected items is returned. This applies even when the colon notation is used to select just one entry, as shown below.

print(tup[0])  # Returns the integer at the first position
print(tup[2])  # Returns the string at the third position
print(tup[1:])  # Returns a tuple with the second and third elements
print(tup[1:2])  # Returns a tuple with the third element
1
three
(2.0, 'three')
(2.0,)

It is possible to construct for-loops as with strings. Sometimes it can be convenient to have the index available, and this is usually done using the len function that provides the length and the function range.

for element in tup:
    print(element)
1
2.0
three
for index in range(len(tup)):
    print(index, tup[index])
0 1
1 2.0
2 three

There is also a short notation for checking if a certain element exists in a list, the in operator.

if 2.0 in tup:
    print("2.0 was found in the tuple!")
else:
    print("2.0 was not found in the tuple!")
2.0 was found in the tuple!

(2) Task: Tuples can be combined to form new tuples in the same manner as strings. Create one tuple from the available tuples below and using slicing so that the end results is the integers from 1 to 5.

A = (4, 5)
B = (2, 3, 4)
C = (1, 8)

Solution:

Hide code cell content
D = C[0:1] + B[0:2] + A
print(D)
(1, 2, 3, 4, 5)

Lists#

Lists are like tuples, but mutable, and can thus be modified. This makes lists preferable in most situations, but we will later see an important exception. Lists are defined using square brackets.

ls = [1, 2.0, "three"]
print(ls)
[1, 2.0, 'three']

Since lists are mutable, the slicing notation can also be used to modify elements of the list.

ls[1] = 1.8  # Change a single element
print(ls)
ls[0:2] = [0.5, 1.5]  # Change two elements in the list
print(ls)
[1, 1.8, 'three']
[0.5, 1.5, 'three']

Elements can be deleted using the del operator, this will not leave a gap in the list, but the remaining indices will be moved in order to avoid a gap.

del ls[1]
print(ls)
[0.5, 'three']

It is possible to construct for loops as with strings/tuples, but be mindful that modifying the length of a list that is being looped can quickly result in unexpected behavior.

print("-- without index --")
for element in ls:
    print(element)

print("---- with index ---")
for index in range(len(ls)):
    print(index, ls[index])
-- without index --
0.5
three
---- with index ---
0 0.5
1 three

As lists and tuples can hold all types, they can even hold other lists or tuples.

new_list = [1, 2.0, [0.1, 0.5], ls, (9, 8)]
print(new_list)
[1, 2.0, [0.1, 0.5], [0.5, 'three'], (9, 8)]

Since lists can be modified, there are a range of useful methods defined for this purpose. Here we demonstrate append that adds an element to the end of the list and sort that sorts lists consisting of numerical entries.

new_list.append(100)
print(new_list)
[1, 2.0, [0.1, 0.5], [0.5, 'three'], (9, 8), 100]
numerical_list = [4, 0.2, 3.8, 91, 6.71]
numerical_list.sort()
print(numerical_list)
[0.2, 3.8, 4, 6.71, 91]

There are more methods available than is reasonable to demonstrate here, but it is important to note that one can get help directly from the language in order to explore such methods. Here we see the help of the list object.

help(numerical_list)
Help on list object:

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:
 |  
 |  __add__(self, value, /)
 |      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.
 |  
 |  __iadd__(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

(3) Task: Given this list of numerical entries, print the five smallest and five largest values.

n_list = [12, 4, 1.2, -3.1, 9.8, 4, 1.2, 2.3, 5.0, 10.0, -1.001]

Solution:

Hide code cell content
n_list.sort()
print("Five smallest entries: ", n_list[:5])
print("Five largest entries:  ", n_list[-5:])
Five smallest entries:  [-3.1, -1.001, 1.2, 1.2, 2.3]
Five largest entries:   [4, 5.0, 9.8, 10.0, 12]

Dictionaries#

Dictionaries (dict in Python) are probably the most important data type in Python, as much of the language is built on them. Like lists and tuples, dictionaries hold a collection of values. But unlike lists and tuples, dictionaries associate each value with a key instead of an index. We specify a key when inserting an item and use the key to access the item again later. Keys are commonly strings, but all immutable types are allowed.

New dictionaries can be defined with curly brackets and colons separate the key and value.

dictionary = {"A": 5, 1: 32, (1, 2): "one_two"}
print(dictionary["A"])
print(dictionary[1])
print(dictionary[(1, 2)])
5
32
one_two

Dictionaries are mutable, so it is possible to change current entries, add new ones and even delete entries.

print(dictionary, "Original dictionary")
dictionary["A"] = 12
print(dictionary, "Modified A")
dictionary["B"] = "C"
print(dictionary, "Added a B entry")
del dictionary["B"]
print(dictionary, "Delete B entry")
{'A': 5, 1: 32, (1, 2): 'one_two'} Original dictionary
{'A': 12, 1: 32, (1, 2): 'one_two'} Modified A
{'A': 12, 1: 32, (1, 2): 'one_two', 'B': 'C'} Added a B entry
{'A': 12, 1: 32, (1, 2): 'one_two'} Delete B entry

The information stored in the dictionary is not ordered in any specific way, and thus it is not possible to use slicing to create smaller dictionaries. It is still possible to loop over the entries, but you may be surprised to learn that the elements returned are the keys and not the values. This does make sense, as the value can easily be reached using the key, but the key can not be reached using the value.

print(dictionary)
for element in dictionary:
    print(element, dictionary[element])
{'A': 12, 1: 32, (1, 2): 'one_two'}
A 12
1 32
(1, 2) one_two

It is often useful to loop over both keys and values. This can be achieved by using the item method which produces tuples with key, value pairs.

for key, item in dictionary.items():
    print("For key", key, "the value", item, "is stored")
For key A the value 12 is stored
For key 1 the value 32 is stored
For key (1, 2) the value one_two is stored

(4) Task: Use a dictionary to set up a shop with some inventory and associated prices. Use a for-loop to create a sale, display the inventory with prices before and after.

Solution:

Hide code cell content
shop = {"usb hub": 150.0, "wireless mouse": 200.0, "laptop": 6000.0, "screen": 2500.0}
print("Prices prior to sale ", shop)
for key in shop:
    shop[key] *= 0.9  # 10% discount
print("Prices during to sale", shop)
Prices prior to sale  {'usb hub': 150.0, 'wireless mouse': 200.0, 'laptop': 6000.0, 'screen': 2500.0}
Prices during to sale {'usb hub': 135.0, 'wireless mouse': 180.0, 'laptop': 5400.0, 'screen': 2250.0}

(5) Task: Given three base colors, e.g. red, green and blue, write a dictionary that contains the names of the resulting color when two different base colors are mixed.

Solution:

Hide code cell content
# Use tuples that contain two of the colors as the keys in a dictionary,
# while the values should correspond to the name of the combined color.
# It is necessary to set up each mix twice,
# as a tuple with its elements reversed is recognized as a different key.

mixes = {
    ("blue", "red"): "purple",
    ("blue", "green"): "cyan",
    ("red", "blue"): "purple",
    ("red", "green"): "yellow",
    ("green", "blue"): "cyan",
    ("green", "red"): "yellow",
}

print(mixes[("blue", "red")])
purple