a = 1
print("Result:", type(a))Result: <class 'int'>
After this lesson you should:
There are several types of data which are used regularly. We have seen most of these already:
int): whole numbersfloat): numbers represented in floating-point format, with finite precisioncomplex): numbers with real and imaginary partsbool): True and False valuesstr): sequences of text charactersNone: the single value of type NoneType, used to represent the absence of a valueTo best understand why Python needs to know the “type” of every piece of data, considering the following:
What does the following produce?:
1 + 2It 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.
Socratica · about 3 min. Meet Python 3’s int, float, and complex types and use type() to identify a value’s type.
The three main numerical data types are:
1 is an integer literal, so its type is int.
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.
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.
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.
Socratica · about 5 min. Explore True, False, and how Python interprets values as true or false.
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))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*True2
and
5.0*False0.0
Of course we can have two bools operate on each other too:
True*True1
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*False0
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.
char for character, but in Python we only have strings.
str. Although, it can be converted to a float using float(b)!
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.
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'>
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:
listdicttuplesetThe 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 (…)))!
Let’s look at each type of collection in more detail.
Socratica · about 6 min. Create lists, access elements by index, take slices, and combine lists.
A list is in Python is pretty much what is sounds like: multiple items grouped together in a linear list.
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])Result: 5
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)Result: [23, 2, 3]
Join lists:
1collections = [1, 2, 3] + [4, 5, 6]
print("Result:", collections)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:
| 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)int and a str (and neither to I!)
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). 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:
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 |
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 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.
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.
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}")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.
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 as the third element of b, at index 2
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 entirely from b and store it in c
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.
Bro Code · about 8 min. Use key/value pairs to look up, add, update, and remove data, then iterate over keys, values, and items.
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:
{}.
[] and a name, which is typically a string.
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){'a': 3, 'b': 5.0, 'c': 'text', 'd': None}
dicts 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:
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.
Bro Code · about 3 min. Learn how tuples store ordered values and how their immutability distinguishes them from lists.
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))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))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] = 4tuple 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
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)*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']
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 herea string
A string
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.
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”.
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
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:
for and in are predefined keywords in Python and they mean exactly what they say.[0, 1, 2, 3, 4] but this could contain anything.: 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:
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.
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.
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.9Convert a given string representation of any number to an int.
a = '11'
answer = # Should be 11Compute the remainder of division between any two numbers
a = 22.1
b = 4
answer = # Should be 2.1Find 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