Introduction to Python 1: the basics

Claudius Gräbner

Exploring Economics Summer Acadamy

Neudietendorf, August

In this introductory lab we will...

  • Learn how to start Python and perform basic mathematical operation
  • Learn about different data types in Python
  • Learn about basic programming techniques, such as if/else statements and loops
  • Learn how to write our own functions
  • Learn about Python's error messages The next intro session will cover the use of classes and then directly moves to networks.

If you start Anaconda Navigator you see the following applications:

In our case, we will use the following applications:

1. Jupyter notebooks: excellent tools to experiment with code and for creating comprehensible descriptions of what you have done

3. Spyder: A python development framewor. It combines a script editor and a terminal, and provides some additional functionality that is useful when you develop new programs. 

3. The terminal of your OS: this will be useful to execute scripts you have written in a text editor or in your IDE before. Note that if you type `python` in your terminal, you launch your python distribution. The Anaconda qtconsole is a similar construct, with additional functionality. 

In case you installed Anaconda, all this is all ready to use! But there are also other good ways to get your Python distribution, an IDE and good script editors. Particularly Linux users might want to use an alternative to Anaconda because they want to exploit the advantages of their Linux packag manager - something that does not work when using Anaconda.

Getting started

There are different ways to start your python environment.

The classic way is to type

python

into your command line.

Alternatively, you could launch the qtconsole from the Anaconda launcher: it provides you with an augmented terminal with improved functionality.

Or you could start a jupyter notebook: you can directly add notes, save your scripts, etc.

Or you can start spyder: an integrated development environment, which comes with an interpreter.

In any case, you now see a terminal, which you can feed with commands.

In [2]:
# Commands after a hash will not be executed
x = 2
y = 4
x + y
Out[2]:
6

Task: experiment with the basic algebra operations.

  • Addition
  • Substraction
  • Multiplication
  • Division
  • Power
  • Taking roots

Play around and find the commands!

In [9]:
2 + 2 # Addition

2 - 4 # Substraction

2*2 # Multiplication

2/2 # Division

2**2 # Power

2**0.5 # Taking roots
Out[9]:
1.4142135623730951

There is a useful technique called the increment operator, which may often come in handy. Instead of writing

a = a + 1

you can equally write

a += 1

This will become useful once we consider loops later on.

In [10]:
a = 10
a = a + 10 # This gives 20
b = 10
b += 10 # This gives 20 as well

So you can use pyhon as a normal calculator. You might have noticed, however, that you can also asign values to variables and use them in later calculations. Associations are always given from right to left: everything that stands on the right side of the equality sign gets associated to the variable on the left side of the equality sign:

In [11]:
x = 4*20
y = 2**2
z = x/y

An association means that you associate a variable name with a given object. Whenever you call the variable, Python follows the association and returns the linked object.

So

x = 2

means that you associate the integer '2' with the variable x. And whenever you type

x

Python follows the association and returns the integer 2.

If you want to see the object that is associated with a variable, use the print statement:

In [12]:
print(z)
20.0

Although every name staring with a letter can be used as a variable name, it is a concention that normal variables should start with a lowercase letter (uppercase letters in the beginning indicate classes, a concept we will learn about later on).

But there are two things that should keep in mind:

  1. Never associate an object with to a built-in function name
  2. Never associate an object with to a built-in class name

Regarding 1:

The function sum sums the elements of a list:

In [6]:
l = [1, 2, 3]
print(sum(l))
6

If you do the following, you will never be able to use this function any more within your current session:

In [ ]:
# sum = 1 + 1
# sum(l) # will print an error because sum is not a function any more, but an integer (2)

If this happens, you have to re-start your Python session.

The same is true for classes:

In [ ]:
# list = [1, 2, 3, 4] # use list as a variable name is a bad idea
# sum(list) # everything is fine
# ...
# list(range(10)) # will produce an error, because you cannot use "list" any more

# int = 50 # Destroys the int class
# int(22.5) # Can't do this any more because int is not the integer 50

Note that you can not only input numbers to the python console, but also text:

In [13]:
x = 'Hello, world!'
print(x)
Hello, world!

Data types

Text and numbers are obviously different. Not all operations for numbers make sense for text. Python distinguishes objects according to their type:

In [14]:
x = 200
y = 2.5
z = "Complexity"
type(x)
Out[14]:
int
In [15]:
# This makes sense:
j = x / y
print(j)
# This does not make sense:
x / z
80.0
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-be1085b31677> in <module>()
      3 print(j)
      4 # This does not make sense:
----> 5 x / z

TypeError: unsupported operand type(s) for /: 'int' and 'str'

We will now learn a bit more about types.

Primitive data types

Primitive data types are the most basic type of data. They store individual elements. Here, Boolean values are probably the most simple type. Boolean values are either True or False and they regularly occur when we test for conditions:

In [16]:
type(True)
Out[16]:
bool
In [ ]:
x = 2
x == 2 # True
print(x < 0.5) # Returns True
z = True
print(x + x == 2) # Returns False
print(True + True) # Returns 2

Notice that if we sum Boolean values, True counts as one, False as zero. This becomes useful later on!

Other primitive data types regularly encountered in programming are integers and floats:

In [18]:
type(2)
Out[18]:
int
In [19]:
type(2.5)
Out[19]:
float

We can convert the one into the other:

In [20]:
x = int(2.5)
y = float(2)
print(x, y)
2 2.0

What is still missing are complex numbers:

In [21]:
x = complex(real=2, imag=4) # or, more concise: complex(2, 4)
x
Out[21]:
(2+4j)

The j notation comes from engineering; in economics it is more common to refer to the imaginary part with i.

The final primitive data type we consider are strings. Here it must be kept in mind that many operations work differently on strings than on numbers:

In [22]:
x = "Hello"
y = "World"
z = x + y
print(z)
4 * x
HelloWorld
Out[22]:
'HelloHelloHelloHello'

Containers

We speak of containers when we refer to data structures that combine primitive data.

An extremely common container is a list:

In [23]:
l = [1, 2, 3, 4] 
type(l)
Out[23]:
list

You can access the elements of a list via squared brackets. Note that in python the first index of a list is zero. This is the same convention as in C, but it is different to R, where the first index is 1.

In [24]:
# print(l) returns [1, 2, 3, 4]
l[2] # The third element
l[1:3] # Elements 2 to 4
l[-2] # The preultimate last element
l[:2] # The first two element
l[:-2] # Everything, except the last two elements
Out[24]:
[1, 2]

Let us look at some useful operations on lists:

In [26]:
l = [1, 2, 3, 4]
len(l) # Gives the length of the list
max(l) # Gives the maximum, if meaningful
3 * l # Repeate the list
l + [99, 99] # Concatenate lists
"Spam" in l # Returns a Boolean value, depending on whether the statement is true
Out[26]:
False

Lists are often useful, in particular in dynamic contexts. This is because lists are mutable. This means that you can take an existing list, and change it:

In [27]:
print("Original list:", l, sep=" ")
l[3] = "New element!"
print("Augmented list:", l, sep=" ")
Original list: [1, 2, 3, 4]
Augmented list: [1, 2, 3, 'New element!']

Thus, you can use square brackets with the index to access particular elements of the list.

As other data structures, lists have methods. A method is called by an object and might be thought as a function that takes the object itself as its first argument:

In [28]:
l = [1, 5, 2, 5, 6, 0]

l.append(15) # appends an element to the list
print(l) returns: [1, 5, 2, 5, 6, 0, 15]

x = l.count(5) # counts the occurences in the list
print("Nb of occurences of 5: ", x) # x is 2

l.extend([1, 2, 3])
print(l) returns: [1, 5, 2, 5, 6, 0, 15, 1, 2, 3]

x = l.index(5) # where is the first instance of 5 in l?
print("First occurence of 5: ", x) # 1

l.insert(3, "Buff") # insert an element to the list
print(l) # returns: [1, 5, 2, 'Buff', 5, 6, 0, 15, 1, 2, 3]

x = l.pop(3) # removes an object from the list and returns it
print("l: ", l, "\nRemoved element: ", x) # x is Buff

l.remove(5) # Removes the first instance of an object from the list
print(l) # returns: [1, 2, 5, 6, 0, 15, 1, 2, 3]

l.reverse() # Reverses the list
print(l) # returns: [3, 2, 1, 15, 0, 6, 5, 2, 1]

l.sort(reverse=False) # Sorts the elements of the list
print(l) # returns: [0, 1, 1, 2, 2, 3, 5, 6, 15]

Note that methods modify the list, and does not return a new list! Being a method of the class list means that it can be called for every list. It might be thought as a function that takes the calling object as its first input. We will consider such subleties later in the course!

The immutable variant of a list is called tuple:

In [29]:
x = (1, 2, 3) # tuple

Much of what works on lists, and does not mutate the list, also works for tuples.

In [30]:
len(x)
Out[30]:
3

The sequence of elements are important for tuples and lists, but it is not for sets. Aside from being unordered, sets cannot contain duplicates:

In [31]:
x = {4, 2, 2, 1}
x
Out[31]:
{1, 2, 4}

All the conventional set operations (e.g. unity, subset, intersection) can be readily evaluated.

The dictionary is also unordered. It contains relations between keys and values, just as, well, a dictionary:

In [32]:
es_en = {}
es_en["Hola"] = "Hello"
es_en["Cerveza"] = "Beer"
es_en
Out[32]:
{'Cerveza': 'Beer', 'Hola': 'Hello'}

Dictionaries are particularly useful once we want to save simulation results!

With these data types in mind we already have a considerable ability to express stuff in python. Now lets look how we can actually do things with these expressions.

First steps in programming

If / then statements

If/then statements are very convenient and frequently used in programming in general, and in ABM in particular:

In [33]:
x = 4
if x == 2:
    print("x equals two!")
elif x < 2:
    print("x is smaller than two")
elif x > 2:
    print("x is bigger than two")
else:
    print("What he heck!?!?")
x is bigger than two

You can also nest if/else statement. This can be useful if you want to check whether a variable is of the right type to perform a certain operation. But keep in mind that too many nested operations are often very bad for the performance of your code.

In [34]:
x = "2"
if type(x)==int: 
    if x == 2:
        print("x equals two!")
    elif x < 2:
        print("x is smaller than two")
    elif x > 2:
        print("x is bigger than two")
    else:
        print("What he heck!?!?")
elif type(x)==str:
    print("Why would you compare strings with ints?")
Why would you compare strings with ints?

You may have noticed the indentation in the if/else statement. In Python, indentation matters ($\neq$ e.g. R).

Whenever you do something wrong with your indentations, Python will return an Indentation error. Many other programming languages do not have this feature. In R, for example, nested code is indicated by brackets. The advantage of the Python approach is that it forces you to write more readable code. The disadvantage is that sometimes your code does not what you want, and from the resulting error message it is not directly obvious that the mistake is a wrong indentation.

Consider the following example:

In [35]:
# You wan to write a program that checks whether a number works as an index for a list
# If it does, the program should return the corresponding element of the list
l = [1, 2, 3, 4, 5]
x = 3.0
if x > 4:
    print("x is too big. List has only five elements!")
    x = int(x)
l[x]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-35-6dff2e3364fa> in <module>()
      6     print("x is too big. List has only five elements!")
      7     x = int(x)
----> 8 l[x]

TypeError: list indices must be integers or slices, not float

It is clear that the original mistake was the indentation: converting x to an integer is necessary, regardless of the if/else statement before! Python informs you about a type error, but in fact it is an indentation error: the line with x = int(x) must not be indended!

This simple example here is pretty clear, but once you write your first ABM you will see that such indentation mistakes can be really difficult to spot! So you need to check your code carefully, even in the area before the error.

It is very helpful to familiarize you with the Python way of using indentation right from the beginning.

Loops

Loops make your life as a programmer much easier. But they can also slow down your code, so you should always search for alternative implementations.

For loops

In a for-loop your programm performs an action with inputs alongside a certain input container. Let's look at an example:

In [36]:
loop_list = ["This", "is", "awesome", "!"]
for element in loop_list:
    print(element, end=' ') # The end='' prevents print() from adding \n at the end
# Guess the output!
This is awesome ! 

For-loops can be nested, and more complicated operations can be conducted within the loop:

In [37]:
loop_list = ["This", "is", "awesome", "!"]
for element in loop_list:
    if element == "awesome":
        print("f****** ", element, end="")
    else:
        print(element, end=' ')
This is f******  awesome! 

A very typical routine is to go through the indices of a list and use the elements of the list as the input for another operation.

Your turn: build a list with the square roots of the integers from 0 to 10 using a for loop!

A useful function in this context is range():

In [38]:
list(range(10))
Out[38]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Actually, range() produces an iterator, another data type, which is useful to speed up your code (more on this in the notes). For now, remember you can use

for i in range(10):
    ...

There is an important distinction between the following two approaches of looping through lists:

In [19]:
l = [1, 2, 3, 4, 5]
l2_ = ["a", "b", "c"]

print("Loop through the elements:")
for i in l:
    print(i, end=" ")
    
print("\nLoop through the indices of the list:")
for i in range(len(l)):
    print(l[i], end=" ")
Loop through the elements:
1 2 3 4 5 
Loop through the indices of the list:
1 2 3 4 5 

Although in this case both procedures give you the same result, it is strongly recommended to use the second option, whenever there is no good reason no to do it.

The reason is that the first approach often leads to undesired results, particularly once your lists become more complex. It also provides you more freedom.

The following simple example illustrates this point.

In [15]:
l_1 = ["a", "b", "c"]
l_2 = [1, 2, 3]
l_3 = [1, 11, 14]

# Now consider you want to do this operation on all lists:
for i in l_1: # This does not work because elements of l_1 are strings
    print("Element called in l_1: ", l_1[i])
    print("Element called in l_2: ", l_2[i])
    print("Element called in l_3: ", l_3[i])  
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-69f70727d3b7> in <module>()
      5 # Now consider you want to do this operation on all lists:
      6 for i in l_1: # This does not work because elements of l_1 are strings
----> 7     print("Element called in l_1: ", l_1[i])
      8     print("Element called in l_2: ", l_2[i])
      9     print("Element called in l_3: ", l_3[i])

TypeError: list indices must be integers or slices, not str
In [16]:
for i in l_2: # This does not work because the elements will be out of range
    print("Element called in l_1: ", l_1[i])
    print("Element called in l_2: ", l_2[i])
    print("Element called in l_3: ", l_3[i])
Element called in l_1:  b
Element called in l_2:  2
Element called in l_3:  11
Element called in l_1:  c
Element called in l_2:  3
Element called in l_3:  14
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-16-2b0a137da211> in <module>()
      1 for i in l_2: # This does not work because the elements will be out of range
----> 2     print("Element called in l_1: ", l_1[i])
      3     print("Element called in l_2: ", l_2[i])
      4     print("Element called in l_3: ", l_3[i])

IndexError: list index out of range
In [17]:
for i in l_3: # The same goes for this...
    print("Element called in l_1: ", l_1[i])
    print("Element called in l_2: ", l_2[i])
    print("Element called in l_3: ", l_3[i]) 
Element called in l_1:  b
Element called in l_2:  2
Element called in l_3:  11
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-17-a29ad86484a5> in <module>()
      1 for i in l_3: # The same goes for this...
----> 2     print("Element called in l_1: ", l_1[i])
      3     print("Element called in l_2: ", l_2[i])
      4     print("Element called in l_3: ", l_3[i])

IndexError: list index out of range

But once you loop through indices, there is no problem:

In [18]:
for i in range(len(l_3)): # This works well:)
    print("Element called in l_1: ", l_1[i])
    print("Element called in l_2: ", l_2[i])
    print("Element called in l_3: ", l_3[i])   
Element called in l_1:  a
Element called in l_2:  1
Element called in l_3:  1
Element called in l_1:  b
Element called in l_2:  2
Element called in l_3:  11
Element called in l_1:  c
Element called in l_2:  3
Element called in l_3:  14

Since this will become an important issue once we work with ABM, you should start looping through indices rather then elements right from the start!

Getting help

Python has a very good documentation. If you are not sure about what a particular function does, check the help:

In [57]:
help(sum) # or "sum?"
Help on built-in function sum in module builtins:

sum(iterable, start=0, /)
    Return the sum of a 'start' value (default: 0) plus an iterable of numbers
    
    When the iterable is empty, return the start value.
    This function is intended specifically for use with numeric values and may
    reject non-numeric types.

Writing nice code

There are many coding conventions that aim to assist programmers in writing code that can easily be understood by others. Once you feel more comfortable about dealing with Python, make sure to read the official guideline.

This guideline, known as PEP8 is part of the so called Python Enhancement Protocols, which can be found here.

Its useful to have a look at them, they contain some interesting information. I really recommend you to have a look at PEP8 because habituating this kind of coding right from the beginning will save you a lot of time later on.

Keep in mind that one of the major criticism of simulation models in economics, particularly agent-based models, is a lack of transparency. Writing nice and readable code is therefore very important to present your models in a transparent way. Python is a very readable language, and PEP8 will help you to exploit this readability.

Outlook

We will continue with the following topics:

  • List comprehensions
  • While loops
  • Writing functions
  • Understanding error messages
  • Building classes

Then we are ready to work with networks!

The next lab we learn about classes and how to handle basic concepts of network theory in Python, using the library NetworkX.