3  Data Types and Data Collections

ImportantLearning Outcomes for this Lesson

After this lesson you should:

  • …understand the details of the different data types
  • …know how to convert between data types, when possible
  • …know the main kinds of data collections or containers and how they work
  • …know the details of using the most common collections
  • …be comfortable with the basic concepts of loops to iterate through collections
Figure 3.1: Some of these actually make sense!

3.1 Data Types

There are several types of data which are used regularly. We have seen most of these already:

  • integers (int): whole numbers
  • floats (float): numbers represented in floating-point format, with finite precision
  • complex numbers (complex): numbers with real and imaginary parts
  • booleans (bool): True and False values
  • strings (str): sequences of text characters
  • None: the single value of type NoneType, used to represent the absence of a value

To best understand why Python needs to know the “type” of every piece of data, considering the following:

What does the following produce?:

1 + 2

It obviously produces 3. In this case Python knows that + means addition.

What does this produce?:

'One' + 'Two'

It produces 'OneTwo'. In this case Python knows that + means join.

Both of these intepretations of the + operator are valid. Python needs to know which one, because 1 + 2 = 12 would be a problem!

Let’s also introduce another one of Python’s built-in functions: type(). As you might guess, this function accepts a variable and returns its type.

a = 1
print("Result:", type(a))
Result: <class 'int'>
b = 2.0
print("Result:", type(b))
Result: <class 'float'>

As we will see below, this function can be used to identify the type of anything, not just variables.

3.1.1 Integers, Floats, and Complex Numbers

NoteVideo: Numeric data types

Socratica · about 3 min. Meet Python 3’s int, float, and complex types and use type() to identify a value’s type.

Watch on YouTube.

The three main numerical data types are:

1a = 1
2b = 2.
3c = 1 + 0.4j
1
1 is an integer literal, so its type is int.
2
Adding a decimal point makes this literal a float, whether we add trailing digits or not. Exponent notation such as 2e0 also creates a float. Both 2. and 2e0 have the whole-number value 2, but their type is still float.
3
Appending j to a numerical literal creates an imaginary number; adding a real part gives a complex number such as 1 + 0.4j. Python uses j for the imaginary unit, a convention also used in engineering.

We can convert between different types using Python’s built-in int(), float(), and complex() functions:

float(1)
1.0
int(1.0)
1

When we convert a finite float to int, Python truncates toward zero rather than rounding to the nearest whole number: int(142.999) is 142, and int(-142.999) is -142. The resulting integer does not retain the fractional part, but conversion does not change the original float. Floats themselves have limited precision, so converting a sufficiently large integer to a float can also lose information.

These conversion rules matter when doing numerical computing.
int(142.999)
142

We can also add imaginary parts to real numbers:

complex(1)
(1+0j)

In this case the imaginary part is 0.

Note that j by itself it not enough. Python will let us use j as a variable name. We can write 1j or 0j and a complex number will result:

type(0j)
complex

If we add an int to a complex, it assumes the imaginary component is 0:

1 + (2 + 2j)
(3+2j)

However, we cannot down-convert a complex number to a float or int:

float(2.0 + 3.0j)

Python is willing to drop decimal place when we convert a float to an int, but drop the imaginary part of a complex number is taking things too far! The above will produce an error message because Python is telling us we broke a rule (or tried to at least). We will explore errors and how to handle them later in this course.

3.1.2 Booleans

NoteVideo: Boolean values

Socratica · about 5 min. Explore True, False, and how Python interprets values as true or false.

Watch on YouTube.

Boolean data is named after the mathematician George Boole who studied the properties of True/False logic. The concept of True and False is very useful in programming for controlling the flow of a program. As we shall see in the next chapter, a program can execute one statement if a condition is True and a different statement if it is False. In Python they are called bool.

1a = False
print("Result:", type(a))
1
The words True and False must be capitalized for Python to recognize them. false would just be treated like a regular variable name.
Result: <class 'bool'>

A more useful way to use bools is to check the result of a comparison like “is this less than that” or “is this equal to that”:

a = 3 < 6
print("Result:", a)
Result: True
a = 1 == 1.0
print("Result:", a)
Result: True

bools can be mathematically treated such that True = 1 and False = 0. For instance:

2*True
2

and

5.0*False
0.0

Of course we can have two bools operate on each other too:

True*True
1

Interestingly, this turns the result into an int. This is because Python “promotes” the bool to an int when it sees that multiplication is being applied. This is same as the conversion of int to float when doing 1 * 2.0.

And:

True*False
0

3.1.3 Strings

Anything between single or double quotes (i.e. ' ' or " ") is a str. The ability to use either type of quote is very handy if we wish to include quotes within the string itself…just use one style to define the str, and the other style inside the str.

1a = ''
2b = '1.0'
3c = 'To begin, begin'
1
Strings can be any length, including 0. In some languages a single letter is called a char for character, but in Python we only have strings.
2
Strings can be any characters we wish, including numbers. Don’t be fooled, this is still a str. Although, it can be converted to a float using float(b)!
3
Strings can, and often do, contain full sentences, including punctuation, spaces, and so on

Since we are talking about conversion between data types, it’s worth pointing out that we can convert a str to a float if it happens to be the text representation of a number:

a = '1.0'
b = float(a) 
print("Result:", type(b))
Result: <class 'float'>

And we can do the same for int:

a = '2'
b = int(a)
print("Result:", type(b))
Result: <class 'int'>

For int(), the string must represent an integer: int('2.0') raises a ValueError, even though float('2.0') works.

3.1.4 None

None is a special object of type NoneType, used to represent the absence of a value. It is distinct from zero, False, and an empty string. Functions that do not return a result explicitly return None; for example, print() writes text but its return value is None:

result = print('Hello')
print(result)
print(type(result))
Hello
None
<class 'NoneType'>

3.2 Data Collections

So far we have created individual variables with statements like x = 1. But many times we would like to group variables together into a container, and store that container in its own variable. This way we can pass around all the values within the container by just passing the container. This is exactly the same motive that inspires us to put physical objects in boxes and bins to carry them around.

In Python these are called collections, and there are several kinds in Python:

  • Lists, or list
  • Dictionaries, or dict
  • Tuples, or tuple
  • Sets, or set

The relationship between data and collections is illustrated in Figure 3.2. It shows a schematic diagram of different data types and collection types, as well as some examples of how they interact. Specifically, how data can be stored inside collections. Also note that collections can be stored inside collections (inside collections (inside collections (…)))!

Figure 3.2: Schematic diagram illustrating the relationship between data and collections.

Let’s look at each type of collection in more detail.

3.2.1 Lists

NoteVideo: Lists and indexing

Socratica · about 6 min. Create lists, access elements by index, take slices, and combine lists.

Watch on YouTube.

A list is in Python is pretty much what is sounds like: multiple items grouped together in a linear list.

Figure 3.3: A typical city street is a linear collection of houses, each with a number which is usually in sequence. A list works in exactly the same way.

Lists are created by grouping values inside square brackets [ ]:

collection = [5, 'a', 2, True, 4.0]
print("Result:", collection)
Result: [5, 'a', 2, True, 4.0]

Python also has a built-in function for creating a list:

collection = list((5, 'a', 2, True))
print("Result:", collection)
Result: [5, 'a', 2, True]

Collections also have a type:

collection = list((5, 'a', 2, True))
print("Result:", type(collection))
Result: <class 'list'>

Each value is located at a numerical index in the list, starting at 0 and ending at len(collection) - 1. The four-item list above therefore has indices 0 through 3. Perhaps you can think of these as street addresses.

We access the value at a given address using syntax like collection[0].

collection = [5, 'a', 2, True, 4.0]
1print("Result:", collection[0])
1
This fetches the item stored at location 0
Result: 5
NoteZero-Indexing vs 1-Indexing

Python uses “0-indexing” meaning all address in things like lists and other number-based collections (i.e. numpy arrays). Humans on the other hand tend to use 1-indexing, so this can be confusing at first.

In 0-indexing, the first item in the list is at location 0, the second item is at location 1, and so on. So instead of thinking about “first” or “second” item, think “item 0 is at location 0”.

We can do a lot of other things with lists too:

Replace items:

collection = [1, 2, 3]
1collection[0] = 23
print("Result:", collection)
1
We can assign to locations using the same syntax we used for fetching
Result: [23, 2, 3]

Join lists:

1collections = [1, 2, 3] + [4, 5, 6]
print("Result:", collections)
1
Note how the addition operator here did something total different than for ints! This is another illustration of why the “type” of a variable matters…because Python will do something different depending on the type.
Result: [1, 2, 3, 4, 5, 6]

We have already seen a small section of Python’s built-in functions which work on single values in Table 2.5, but Python also includes other functions meant to operate on lists (and collections in general as we’ll see). The full list is given in Table A.2, but a partial list of functions that can be applied to a collection is given below:

Table 3.1: The built-in functions in Python relevant to collections
Function Description
all() Returns True if every item is truthy; also True for an empty iterable
any() Returns True if at least one item is truthy; False for an empty iterable
len() Returns the length of an object
max() Returns the largest item in an iterable
min() Returns the smallest item in an iterable
reversed() Returns a reversed iterator
sorted() Returns a sorted list
sum() Sums the items of an iterator

An item is truthy if Python treats it as true in a condition. Nonzero numbers are one example; we will explain truthiness in Section 4.1.

Here we can see some of these in action:

collection = [4, 3, 1, 0, 2]
collection = sorted(collection)
print("Result:", collection)
Result: [0, 1, 2, 3, 4]
collection = [4, 3, 1, 0, 2]
value = sum(collection)
print("Result:", value)
Result: 10
collection = [4, 3, 1, 0, 2]
length = len(collection)
print("Result:", length)
Result: 5

Note that if we try a function like sum on a list containing incompatible data, Python will complain:

collection = [10, '10']
1value = sum(collection)
1
This will raise an error because Python does not know how to add an int and a str (and neither to I!)
WarningObject Oriented Programming is Awesome, but Confusing at First

We can no longer avoid talking about objects and Object Oriented Programming (OOP).

The word object is meant to conjure up images of physical objects that have properties like colors, shapes, lids and handles. Thinking about piece of code or data as a physical objects actually helps us understand the code better, even though it’s confusing at first.

OOP has several defining features. For our current purposes, as an “end-user”, an object is a piece of data and it carries with it as set of functions that act on that data (i.e. itself). It works like this:

We can use Python’s sorted function:

collection = sorted([2, 3, 1, 0])
print("Result:", collection)
Result: [0, 1, 2, 3]

Or we can use the function, actually called a method, that comes with the list:

collection = [2, 3, 1, 0]
1collection.sort()
print("Result:", collection)
1
The . accesses the list’s sort method, just as math.acos accesses the acos function in the math module. collection.sort() changes the list in place and returns None; sorted(collection) returns a new sorted list. Do not write collection = collection.sort(), since that would replace the variable’s list with None.
Result: [0, 1, 2, 3]

So in addition to supplying an object to a function, you can also call an object’s method to accomplish the same result.

dir(collection) lists attribute names, including methods. Names with double underscores at both ends, such as __len__, are special or dunder names used by Python’s protocols, not private methods. For example, len(collection) uses the list’s length protocol. The list has the following ordinary methods:

Table 3.2: List of methods on list objects
Method Description
append Adds a new item to the end itself
clear Deletes all items from itself
copy Returns a shallow copy (nested objects are still shared)
count Returns the number of times the given value occurs itself
extend Joins another list to the end of itself
index Returns the first location of the given value
insert Inserts a value before the given index, shifting later items
pop Removes the item at the given address and returns it
remove Removes the first occurrence of the given value
reverse Reverse the order of itself
sort Sorts the values in place

3.2.1.1 Advanced Indexing

The follow subjects are introduced here so we can begin to explore them and get familiar. They will be re-visited at several other points in this course, with increasing levels of sophistication.

Slice Notation

It is possible to retrieve “sections” or “segments” of a list using “slice notation”. Slice notation uses the colon (:) to indicate you want a range of values:

collection = [5, 'a', 2, True, 4.0]
1print("Result:", collection[1:3])
1
This returns items 1 through 3, not including 3, in a new list.
Result: ['a', 2]

There are a a few useful variations on this : notation. 1. If you omit the first number, it assumes the start of the list. So :3 gets elements 0, 1, and 2 (not 3) 2. If yo omit the last number, it assumes the end of the list. So 3: gets elements 3, 4 and so on, including the last element.

We will learn “a lot more” about slicing when we talk about Numpy arrays later in the course.

NoteSlice Objects

One of Python’s built-in functions is slice. It returns a “slice object” which can be used to index into a list. Is is the long-form version of the colon symbol.

a = [0, 1, 2, 3, 4, 5, 6, 7, 8]
1s = slice(0, 3)
2print(a[s])
b = [5, 6, 7, 8, 9]
3print(b[s])
1
The benefit of a slice object is that it can be stored and passed around your program.
2
Here we use s to index into a
3
Here we reuse s to index into b
[0, 1, 2]
[5, 6, 7]

Reverse Indexing

We often want to get elements at the end of a list instead of the beginning. Python has a trick for us: use negative index values:

a = [0, 1, 2, 3, 4]
1b = a[-1]
c = a[-2]
print(f"The value in location -1 is {b}")
print(f"The value in location -2 is {c}")
1
When indexing in the forward direction, Python starts at 0; however, in the reverse direction it starts at -1.
The value in location -1 is 4
The value in location -2 is 3

Picture the list as in Figure 3.4: index -1 selects the last item, and -2 selects the second-last. This is a memory aid, not circular indexing: indices do not wrap indefinitely. For a five-item list, valid indices run from -5 through 4; a[-6] and a[5] raise IndexError.

Figure 3.4: Conceptual diagram illustrating the logic of negative indexes in the 0-based indexing scheme

Multi-Dimensional Indexing

A list can contain any other type of data within it, including other lists! This introduces the concept of multi-dimensional indexes:

a = [1, 2, 3]
1b = [4, 5, a]
2c = b[2][1]
print(c)
1
Here we have embedded a as the third element of b, at index 2
2
We can access items in a by first indexing into b at index 2, then we start accessing items in a
2

The above ‘chained’ indexing is equivalent to:

a = [1, 2, 3]
b = [4, 5, a]
1c = b[2]
2d = c[1]
print(d)
1
Here we actually fetch a entirely from b and store it in c
2
Now we can index into c using the “normal” indexing, but the result is the same
2

We will see multi-dimensional indexing again when we talk about Numpy arrays in later chapters.

3.2.2 Dictionaries

NoteVideo: Dictionaries

Bro Code · about 8 min. Use key/value pairs to look up, add, update, and remove data, then iterate over keys, values, and items.

Watch on YouTube.

A dictionary, or dict, maps keys to values. Keys are often descriptive strings, but may also be other hashable objects, such as integers. Unlike a list index, a dictionary key identifies an entry rather than its position; lists cannot be keys because they are mutable and unhashable.

The most useful way to create a dictionary is to define an empty one, then fill it with items:

1d = dict()
2d['item 1'] = 'foo'
d['item 2'] = 'bar'
3d['item 1'] = 'baz'
print("The dictionary contains:", d)
1
We can create also create an empty dictionary using empty curly braces {}.
2
Once the dictionary is initialized we can add as many items as we wish using square brackets [] and a name, which is typically a string.
3
We can also overwrite items
The dictionary contains: {'item 1': 'baz', 'item 2': 'bar'}

The best part about using dicts is that we can use these descriptive names to store and access values:

d = dict()
d['temperature'] = 298.0
print("The temperature is:", d['temperature'])
The temperature is: 298.0

We can also create a dict on one line by providing the “key-value” pairs inside curly braces {}.

1d = {'a': 3, 'b': 5.0, 'c': 'text', 'd': None}
print(d)
1
Each item in this statement is called a “key: value” pair, since you must supply a name (“key”) for each piece of data (“value”).
{'a': 3, 'b': 5.0, 'c': 'text', 'd': None}
Notedicts are one of Python’s most useful features

dicts are used everywhere in Python programs. What makes them so useful is that you know what is stored in each location, as long as you give it a meaningful name (key). If you really wanted, you could even include units in the dictionary key, like d['pressure (Pa)'] = 101325. You can’t get much more self explanatory than that! Because they are so useful, they get used a lot in almost all Python projects of any complexity.

Python itself also uses dictionaries. Some objects expose their writable attributes through __dict__, but this is not universal: ordinary integers, strings, lists, and dictionaries do not have an instance __dict__. We will return to object attributes in the OOP chapter.

You can apply Python’s built-in functions as we did with the list:

d = {'a': 3, 'b': 5.0, 'c': 'text', 'd': None}
length = len(d)
print("Result:", length)
Result: 4

Using dir() we can inspect a dictionary’s attribute names, including its methods:

Table 3.3: List of methods on dict objects
Method Description
clear Removes all items from itself
copy Returns a shallow copy of the dictionary
fromkeys dict.fromkeys(keys, value) creates a new dictionary with the same value for each key (default None)
get d.get(key, default) returns the value, or the default if the key is absent (default None)
items Returns a dynamic view of the (key, value) pairs
keys Returns a dynamic view of the keys
popitem d.popitem() removes and returns the last inserted (key, value) pair; raises KeyError if empty
pop d.pop(key) removes and returns that key’s value; an optional default handles an absent key
setdefault d.setdefault(key, default) returns the existing value, or inserts and returns the default for that key (default None)
update Adds or overwrites entries from another mapping or iterable of key/value pairs; returns None
values Returns a dynamic view of the values

One especially useful method is keys(). It returns a view, which reflects later changes to the dictionary. Use list(d.keys()) when you need a separate list of the keys:

d = {}
d['T'] = 298.15
d['P'] = 101325
print("The keys are:", d.keys())
The keys are: dict_keys(['T', 'P'])

For an absent key, d['humidity'] raises KeyError, while d.get('humidity', 'not measured') returns 'not measured' without inserting a key. In contrast, setdefault() inserts the default when the key is absent.

Example 3.1 (Using a dict in a calculation) Use the ideal gas law to compute molar density given the needed values are supplied in a dict.

Solution

vals = {'P': 101325, 'T': 298, 'R': 8.314}
n_V = vals['P']/(vals['R']*vals['T'])
print("The density is", n_V)
The density is 40.896894217403165

Comments

The ability to combine all related variables into a single object, with each variable given its own descriptive name is very powerful. When programs get more complicated, it really helps keep things organized and understandable.

3.2.3 Tuples

NoteVideo: Tuples

Bro Code · about 3 min. Learn how tuples store ordered values and how their immutability distinguishes them from lists.

Watch on YouTube.

A tuple is a sequence like a list, but its entries cannot be replaced, added, or removed after creation: the tuple is immutable. This does not make its contents recursively immutable. If a tuple contains a list, that list can still change.

A tuple is usually written as comma-separated values inside round brackets ( ):

tup = (1, 2.0, 'text')
print("Result:", type(tup))
Result: <class 'tuple'>

Python also has a built-in function for creating tuples:

1tup = tuple([1, 2.0, 'text'])
print("Result:", type(tup))
1
Here we have converted a list (defined by the square brackets) into a tuple
Result: <class 'tuple'>

It is the commas that form a nonempty tuple, not the brackets alone. Thus (1,) is a one-item tuple, whereas (1) is just the integer 1. The empty tuple is written (). In an assignment, we can omit the brackets around comma-separated values:

1tup = 1, 2.0, 'text'
print("Result:", type(tup))
1
Python just assumes we want to package these 3 variables together
Result: <class 'tuple'>

We can access the items in the tuple by their numerical location, just like with a list:

tup = 1, 2.0, 'text'
print("Result:", tup[0])
print("Result:", tup[1])
print("Result:", tup[2])
Result: 1
Result: 2.0
Result: text

But we cannot write to any locations. The following will result in a TypeError:

tup = 1, 2.0, 'text'
1tup[0] = 4
1
Item assignment to a tuple is not allowed since they are “immutable”

You can “unpack” a tuple into individual, separate variables using the following convenient syntax:

tup = (1, 2.0, 'text')
a, b, c = tup
print("Result:", a)
print("Result:", b)
print("Result:", c)
Result: 1
Result: 2.0
Result: text
NoteNext level Tuple unpacking

And if you really want to get into the weeds you can partially unpack it:

tup = (1, 2.0, 'text')
1a, *b = tup
print("Result:", a)
print("Result:", b)
1
The *b collects the remaining items into a new list, even though the input is a tuple, and assigns that list to b.
Result: 1
Result: [2.0, 'text']

3.2.4 Strings Are Basically Collections

Strings behave very similarly to collections. They obviously have a length:

s = 'a string'
print("Result:", len(s))
Result: 8

They can be indexed for reading data:

s = 'a string'
print(s[0])
a

And they can be “sliced” to extract sub-strings:

s = 'a string'
print(s[0:5])
a str

Strings are immutable, so s[0] = 'A' raises a TypeError. Methods such as replace() return a string with the requested changes; they do not modify the original string:

s = 'a string'
changed = s.replace('a', 'A')
print(s)        # Original string is unchanged
print(changed)  # Result of the replacement
s = changed     # Reassign the name if we want to keep the result here
a string
A string

3.3 Introduction to Loops

3.3.1 For-Loops

NoteVideo: For-loops and range

Bro Code · about 5 min. Practise iterating over range() and strings, including steps and reversed iteration. The final examples preview continue and break from the next chapter.

Watch on YouTube.

In Chapter 2 it was mentioned that computers are crazy fast at calculating things. One of the most common ways to use this ability is to have the computer scan through a list of items and perform some computation on each item. This is done using a “for-loop”.

NoteFor-Loops are the OG of programming

Repeating work is one of the building blocks of programming. Many languages provide for-loops; others express repetition using different constructs. Python’s for-loop lets us process the items of an iterable one at a time.

The for-loop has the following syntax:

for i in [0, 1, 2, 3, 4]:
    print(i)
0
1
2
3
4
NoteIterating

Scanning through a collection is called iterating in Python. The dictionary definition of “iterate” is “to do (something) over again or repeatedly”.

Objects such as lists and dicts are said to be iterable because they contain multiple items over which iteration can be performed. If you try to iterate over a non-iterable object, like an int, Python will complain.

First let’s unpack the first line:

  • The for-loop syntax in Python is almost equivalent to spoken English. for and in are predefined keywords in Python and they mean exactly what they say.
  • The loop will repeat for each item in the given list. In this case the list is [0, 1, 2, 3, 4] but this could contain anything.
  • The : at the end of this line indicates that the logic of the loop is about to begin.

Now let’s look at the second line:

  • The logic inside the for-loop is indented by 4 spaces according to the whitespace rules discussed here: Important 7.1.
  • The value of i will change on each loop. It will take on each of the value in the given list in order ([0, 1, 2, 3, 4] in this case).

Let’s look at a few more variations on the for-loop. Firstly, we can use i to index into a list:

values = ['for', 'loops', 'are', 'fun!']
for i in range(len(values)):
    print(values[i])
for
loops
are
fun!

Here we have used range() and len(). len(values) is 4, so range(4) creates a range object representing the integer sequence from 0 up to, but not including, 4. It is not a list; use list(range(4)) if a list is needed. In the loop, i takes the values 0, 1, 2, and 3, which we use to index into values and print each item.

We can make things a bit more concise. Instead of generating indices, let’s just “iterate” over the actual list:

values = ['for', 'loops', 'are', 'fun!']
for item in values:
    print(item)
for
loops
are
fun!

Here we can see that Python assigns each successive word in values to item on each time through the loop.

3.4 Assignment 2

The following 10 questions should be answered by placing 10 cells in the same .ipynb file named A2.ipynb. They will be marked by running each cell on a variety of random inputs, and checking for correctness. Each cell must be named by putting a comment at the top like "# A2Q1"and so on, where A2 refers to assignment 2, and Q1 refers to question 1.

3.4.1 Installing/Updating and Using the Checker

The assignment will be graded using an automated systems that runs your notebooks. To ensure that your assignments are formatted properly and the code is correct, you may wish to use the ‘checker’. The instructions for installing and using the checker can be found in Appendix E.

Convert a given string representation of any number to a float.

a = '44.9'
answer =  # Should be 44.9

Convert a given string representation of any number to an int.

a = '11'
answer =  # Should be 11

Compute the remainder of division between any two numbers

a = 22.1
b = 4
answer =  # Should be 2.1

Find the integer and decimal components of a number and store them as a tuple:

a = 44.3
answer =  # Should be (44, 0.3)

Given a value of arbitrary type, find the type and express it as a string.

a = [1, 2, 3]
answer =  # Should be "<class 'list'>"

Given two lists, joins them into a single list.

a = [1, 2, 3]
b = [4, 5, 6]
answer =  # Should be [1, 2, 3, 4, 5, 6]

Given a dictionary, a key and a value, inserts the value into the location given by key.

a = {}
key = 'name'
val = "Ada"
answer =  # Should be {'name': "Ada"}

Given a list and a value, deletes the first instance of that value from the list.

a = [1, 2, 1, 3]
val = 1
answer =  # Should be [2, 1, 3]

Given a list, extract a sub-list which contains only the the first half of the values, rounding down if the list is an odd number of elements long.

a = [1, 2, 3, 4, 5]
answer =  # Should be [1, 2]

Given a list containing only numerical values, find the difference between the minimum and maximum values it contains.

a = [1, 2, 3, 4, 5]
answer =   # Should be 4