Script 1 for the video lectures accompanying the seminar "Economic Methodology and Agent-Based Modelling in Python" by Claudius Gräbner
For the course homepage see: https://claudius-graebner.com/teaching/introduction-abm-deutsch.html
This is the script for the video introduction to the Python programming language. It is addressed to beginners who have no previous experience in programming.
In any case, I suggest you have a quick look at a primer on how what programming languages do and how they work in theory. This will help you understand their functioning.
But lets get started!
First thing you will need to understand is how to issue commands to Python. If you type stuff in the console and press enter, this means you tell Python to execute (or 'to compute') what you have just typed.
If what you have typed before is consistent with the Python syntax, Python will obey and execute the command:
2 + 2
5*12
print("Hello!")
Yet, if you write a command that is not consistent with the Python syntax, or contains any other kind of error, your computer is going to complain and throw an error message at you - something you might not understand at the moment, but something that is actually pretty useful:
printsomething!
Python can be used for a wide range of tasks, such as machine learning, data visualization, econometrics, agent-based modeling, text mining, web scrapping, etc.
Yet we will start low and first use the console as a simple calculator.
For example, you can ask Python to solve basic mathematical operations.
To do so, you can make use of basic mathematical operators
.
Operators are symbols that tell Python to carry our a particular form of computation:
2 + 2 # Addition
2 - 4 # Substraction
2*2 # Multiplication
2/2 # Division
2**2 # Power
2**0.5 # Taking roots
Note that I have written some stuff after a '#'. In Python '#' indicates a comment. This means that everything that comes after the '#' will not be executed by the computer. So you can use it to make annotations and explanations of your code.
Later, when you write models, commenting on them extensively is essential if you ever want others to understand your code, or get back into your code after some time of absence!
Writing commands into the console is not always useful, for example when you really use Python as a calculator. But this is not what the language is built for!
Whenever you want Python to solve some more complex tasks, you are obliged to write scripts.
Scripts are text documents that contain Python code. It is easy to tell your computer to execute these scripts. This basically means that you ask your computer to read the script and execute every line of the code - almost as if you had written every line into the console and pressed enter (you can actually copy-paste the code into the console and press enter, but this is usually a bad idea, for many reasons).
When you write a script containing the following lines and execute it you might be surprised by the outcome:
3*4
5 + 9
9 - 10
Python only returns the result of the ultimate line.
Whenever you want to read the results of all calculations you must tell Python to print
all the results.
This can be done by the print
function.
Functions
are pre-defined algorithms that do stuff for you.
The print
function prints whatever you pass to it as an input.
To pass input to a function, write the function name, followed by brackets containing the input, also called the arguments of the function:
print(3*4)
print(5 + 9)
print(9 - 10)
Since we are now working in a script you can provide Python with some more complex tasks. In particular, you can issue some mathematical task, let Python store the result in an object, and then process this object further.
This is really useful:
x = 5 + 12
y = 8**2
z = 6*x + (100-y)
print(z)
An association in Python works via the =
command.
Whatever is written on the right side of =
gets associated with the object mentioned on the left side of the =
.
The name of the object is also called identifier
.
For example, in the previous code junks, the letters 'x', 'y', and 'z' were identifier. If you call them via the console, they help Python to identify the object you want to get, in the case of 'x' the 'identified' object is the integer 17:
x
There are a few rules for valid identifiers: basically, any combination of lowercase (a to z) or uppercase (A to Z) letters, digits (0 to 9) and underscores (_) can be used for an identifier.
Yet, there are also a few exceptions:
keywords
as identifiers. Keywords are symbols or strings that perform a very particular and important role in Python, and must not be overwritten. There are 33 keywords in Python (and we will learn how to use some of them later):import keyword
keyword.kwlist
Identifiers must not start with a digit.
Identifiers must not contain symbols other than letters, digits, and underscores. Special symbols such as '!' are not admissable (which is why all operators cannot be part of an identifier).
Additionally, there are some conventions. For example, simple associations should not start with an uppercase letter (this is reserved for the definition of classes, more on this later).
Also, it is not recommendable to use names that are already associated with functions, such as print
. If you associate something with an object called print
you basically override the reference to the print
function - and you most likely want to avoid this!
To see the association of the name print
with the print function, just type print
without the brackets:
print
While we have so far only used numbers, there is nothing that prevents us from letting Python print other things, such as strings:
print("Hello!")
But what is actually the difference between a 'number' and a 'string'?
To better understand this, we need to learn about data types
.
For your programming work it is important to know about different types
.
This is because several operators, functions or keywords only work for some types, or their effect on different types is different.
Therefore, we now learn about some of the most common and important types. For the sake of simplictly (and theoretically not 100% correct) we distinguish between primitive types and containers, whereby container are collections of potentially numerous primitive types.
The primitive types we will have a look at are:
The containers we consider at this stage are:
To get the type of an object you might use the function type
:
type(3)
Fact for the future: This essentially tells us that 3
is an instance of the class int
.
Classes are blueprints for certain objects with particular properties and will become absolutely essential for agent-based modelling later. We will learn how to define our own classes in one of the next sessions.
Integers are abbreviated as int
in Python.
type(3)
On integers you can perform all the typical mathematical operations, such as addition, substraction, multiplication, etc.
Also, integers are the output of many functions, e.g. functions that count stuff (more on this below).
As integers, floats are numbers, but they come with decimal places:
type(3.5)
As for integers, all conventional mathematical operations are defined for floats.
It is also possible to transform integers to floats, and vice versa:
x = 2
type(x) # an integer
y = 2.5
type(y) # a float
x = float(x) # transforms x into a float
y = int(y) # transforms y into a float
y
Keep in mind that while transforming an integer to a float usually comes without problems, some information is lost when transforming floats to integers: all the decimal places are simply removed.
Thus, when you sum floats and integers, the result will be a float, such that no information gets lost - even if the result has only zero decimal places:
z = 2.0 + 8.0
type(z)
You might ask yourself why Python distinguishes between integers and floats, and not simply uses floats only. There are mathematical and logical reaons for this. As in mathematics, integers are required whenever you want to count stuff, or when you want to tell Python to do something x times (see below), or if you want to use them as indices. We will soon encounter several examples where the distinction gets illustrated hands on.
Strings usually behave very differently than integers and floats.
For example, not all the conventional mathematical operations are defined for strings:
x = "a string" # strings are defined by quotation marks at the beginning and the end
y = "another string"
x - y
Other mathematical operations, however, work for strings, but in a different way than on floats or integers:
x + y
Some operations work with integers and strings:
5 * x
Note that this does not work for floats and strings:
5.0 * x
There are two different boolean values: True
and False
.
x = True
type(x)
They naturally occur when you test for relations such as 'greater than', 'smaller than' or 'equal to':
5 > 6
8 == 8.0
Interestingly, boolean values sometimes behave like integers:
True + True + False
Summing up boolean values returns the number of True. This is useful in many instances.
We now turn to containers. As indicated above, containers can contain a collection of primitive data types:
x = [1, 50, "Bla", 5.0]
type(x)
A list can contain an arbitrary number of primitive data types, which do not necessarily need to be of the same type.
In fact, a list can also contain other containers, e.g. lists:
y = [4, 2, x, "blubb"]
y
You can count the number of elements in a list using the function len
:
len(y)
If you want to access certain elements of lists you can do this by supplying the corresponding index. There are two things to keep in mind:
x = [1,2,3,4]
x[0] # The first element of x
print(y)
y[2]
x[2.0]
You can also mutate lists by replacing certain elements:
print(y)
y[1] = 5000
y
You can also nest this operation:
y[2][1] = "New element!"
y
But this does not work if the index is greater than the length of the list:
x = [1,2,3]
x[3]
But you can access the last element of the list by using negative indices:
print(x)
x[-1]
If you want to concatenate lists by using addition:
x = [1,2,3]
y = [4,5,6]
x+y
And you can also 'multiply' lists (again, only using integers):
3 * x
In principle, tuples are similar to lists, with one important distinction: Lists are mutable, tuples are not
l = [1,2,3,4, "bla"]
l[2] = "Stuff"
print(l)
# But:
t = (1,2,3,4, "bla") # Note the different brackets used to define tuples!
t[2] = "Stuff"
Testing whether a certain element is in a tuple is also straightforward:
"bla" in t
Sets are created using the set
function, or by placing elements into curly brackets.
To create an empty set, however, the set
function must be used since {}
creates an empty dictionary.
set_1 = set([1,2,5,6]) # note that the input is a list, not simply the elements
set_2 = {2,6,1,9}
set_3 = set()
Sets can be distinguished from tuples and lists by the following properties:
# ad 1: sets cannot contain duplicate elements
set_1 = {1,2,5,3,3,3}
print(set_1)
# ad 2: sets can contain elements of different data types
set_2 = {2, 5, "bla"}
print(set_2)
# ad 3: The ordering of their members is not fixed
set_2 = {9, 8, 7, 6}
print(set_2)
# ad 4: Sets are mutable (just as lists), but indexing does not work
set_2 = {2, 5, "bla"}
set_2.add("blubb") # we will learn about the logic behind this syntax below
set_2.remove(2)
print(set_2)
set_2[2]
Sets are useful for membership testing, identifying unions, differences or intersections.
set_1 = {1, 5, 9}
set_2 = {99, 1, 4}
3 in set_1
set_1 - set_2 # difference
set_2 - set_1 # difference
set_1 | set_2 # union
set_1 & set_2 # intersection
set_1 ^ set_2 # symmetric_difference
Dictionaries are among the most useful data types in Python, although their behavior might seem to be a bit counter-intuitive in the beginning. But don't worry, after a bit of practice you will be able to use dictionaries effectively - and this will prove useful in a wide array of situations.
Dictionaries are associations between 'keys' and 'values'.
An empty dictionary is created by either {}
or the function dict
.
dict_1 = {} # alternative: dict_1 = dict()
dict_1
Then you can fill the dictionary by adding key-value assocations like this:
dict_1["First_Key"] = "The value"
print(dict_1["First_Key"])
print(dict_1)
Keys must be unique, but values do not need to be unique. This makes sense: think about a dictionary in which you want to look up the key 'super'. If there were multiple keys called 'super', which value would you expect Python to return?
dict_2 = {"super" : "bad", "super" : "good"} # the second key will overwrite the first
dict_2
On the other hand, there is no problem with both the key 'super' and 'great' both mapping to the value 'positive':
dict_2 = {"super" : "positive", "great" : "positive"}
dict_2
You can use everything as a key as long as it is an immutable object (which itself does contain only immutable objects):
dict_2 = {"super" : "positive", "great" : "positive", 2 : "Two"}
dict_2
dict_2 = {"super" : "positive", "great" : "positive", [1,3] : "Two"}
# Does not work because a list is mutable
To identify all different keys, or all the values of a dictionary you can use the methods keys
and values
(more on the concept of a method below):
dict_2.keys()
dict_2.values()
This way, it is also easy to check whether a certain key is part of a dictionary:
"super" in dict_2.keys()
"super" in dict_2.values()
Functions are useful: they take an input (one or more 'arguments'), execute an operation on the input, and return an output.
We have already encountered a number of function that are defined within the base code of Python (called 'builtin function' because they are already built into Python).
For example, the print
function takes one argument and prints it to the console:
type(print)
print(2) # The input is 2
The syntax for using functions is always the following:
first, type the function name, then directly add the arguments in normal brackets.
Some functions take more than one argument.
If you do not know what a certain function does, you might use the help
function to find out (althought the output might still look somehow crypted):
help(print)
The easiest way to understand how functions work is to build your own function.
This is done via the keyword def
.
Here we will use the following example and go through it one by one:
def test_function(x, y):
"""
This is a function that sums up
two numbers and returns the result
as a float.
Parameters
----------
first : int or float
A first number
second : int or float
A second number
Returns
-------
float
The sum of the two parameters
"""
result = float(x + y)
return result
As you can see, the function definition consists of the following parts:
def
indicates the beginning of the function definition. test_function
. This will be what you need to type if you want to call the function later. If you define a new function with the same name as a previously defined one, Python overrides the reference to the older function, so be careful. Function identifiers should contain only lowercase latters.x
and y
. result
.result
.This function is now ready for use:
test_function(5,5)
Note that all definitions made within the function definition are local. They are discarded once the function was called.
bla = test_function(2,4)
result # will throw error since 'resuls' is defined only within the function
Sometimes, a function has an default value for some parameters. If there is no value specified to them explicitly, the function uses the default value:
def test_function_2(x, y, z=10):
"""
This is a function that sums up
two numbers and returns the result
as a float.
By default, the result is also
multiplied by 10.
Parameters
----------
first : int or float
A first number
second : int or float
A second number
third: int (optional)
A third number
Returns
-------
float
The sum of the two parameters times z.
"""
result = float(x + y) * z
return result
print(
test_function_2(2,3, z=100)
)
print(
test_function_2(2,3) # Now the default value for z is used!
)
Everything in Python is an object.
We will learn more about objects later when we discuss classes
, but we will preview some of the content at this point.
Objects in Python have to major components, attributes and methods. Methods are basically functions that are called from within the object itself, and take the object as their first argument.
They usually perform operations that are very common for the specific object at hand.
For example, when you work with lists, a very common task is to append and element to the list.
Therefore, the object type list
has a method append
that does exactly this:
list_1 = [1,4,2,6,5]
print(list_1)
list_1.append(3)
print(list_1)
Another common task is to sort a list, and this is what the method sort
is for:
print(list_1)
list_1.sort()
print(list_1)
list_1.sort(reverse=True) # as with functions, you can also give keywords to methods
print(list_1)
For the sake of illustration, here are some methods for lists that are quite useful. For other types you might want to look up the relevant documentation.
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]
It is very important to keep in mind that methods do not create a new object, but mutate the original object!
There are a number of reasons for why writing functions is a good idea:
For these, and for many other reasons, there is a principle in software engineering called DRY, which stands for "don't repeat yourself"! Adhering to the DRY principle means to avoid WET solutions, for which writing your own functions is essential. You may read the wikipedia article on the principle, its SIF (short, interesting, and fun) and tells you what WET stands for.
If/then statements are very convenient and frequently used in programming in general, and in scientific applications in particular:
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!?!?")
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.
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?")
You may have noticed the indentation in the if/else statement. In Python, indentation matters ($\neq$ e.g. R). This forces you to write well-structured and readable code - at least to some extent;)
Assertions are somehow related to if/else statements, and they are very useful, in particular in the context of more complex models.
Assertions are used to make sure certain mistakes do not happen, or at least do not remain unnoticed.
An assertion allows you to test whether a certain condition holds, and produces an AssertionError if it doesn't. Thus, it prevents the program from moving on, until the conditions are favorable.
x = [1, 3, 5, -1]
assert len(x)<3, "This list is too long!"
print(len(x))
Note that you can write your own error text for assertion errors. This facilitates bugfixing enormously.
Loops are a feature of almost every programming language. There is a good reason fo reason for this: they are incredibly useful since they allow you to automate repetitive tasks.
There are two main types of loops in Python: for-loops
and while loops
.
We will also encounter something very 'Pythonic':
list comprehension
, a very useful tool similar to for loops that helps you to speed up your code, and to make it more readable.
Unfortunately, there is one thing to keep in mind: even if loops are often very useful, they are also slow, and should be avoided whenever possible. List comprehensions are slightly faster, but should also used sparsely.
In a for-loop your programm performs an action with inputs alongside a certain input container. Let's look at an example:
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!
Note that the word after the keyword for
is (almost) arbitrary.
The following code does exactly the same thing as the code above:
loop_list = ["This", "is", "awesome", "!"]
for bla in loop_list:
print(bla, end=' ')
For-loops can be nested, and more complicated operations can be conducted within the loop:
loop_list = ["This", "is", "awesome", "!"]
for element in loop_list:
if element == "awesome":
print("f****** ", element, end="")
else:
print(element, end=' ')
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.
For example, we might 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()
:
list(range(10))
Actually, range() produces an iterator, another data type, which is useful to speed up your code (more on this in the notes).
type(range(10))
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:
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=" ")
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 argument against it.
The reason is that the first approach often yields undesired results, particularly once your lists become more complex, or when you do more than one thing within a loop. And this will happen often in the context of ABM, since you often use loops to execute operations at every time step.
The following simple example illustrates the advantage of looping through indicators.
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])
But once you loop through indices, there is no problem:
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])
Since this will become an important issue once you are concerned with more advanced tasks, you should start looping through indices rather then elements right from the start!
But keep in mind: for loops are slow and should be avoided whenever possible.
Python has an awesome technique called list comprehension
.
base_list = [3, 2, 5, 7]
new_list = [x*10 for x in base_list]
new_list
base_list = range(10)
roots = [x**0.5 for x in base_list]
roots
Sometimes it is useful to combine a list comprenehsion
and an if/else
statement.
For example, suppose you want a list with the squares of the even numbers from 1 to 20.
You could either use arange(2,20,2)
to create an array with the even numbers, and then use list comprehension on this.
Of you can add an if/else statement using the function floor
from the math
library (more on libraries below).
This function rounds off a number, so $2\cdot\text{floor}(i/2)=i$ if and only if $i$ is an even number.
from math import floor
even_squares = [i**2 for i in range(20) if 2*floor(i/2) == i]
print(even_squares)
The final programming technique we consider are while loops
.
They are less frequently used than for loops, but may come in handy.
In a while loop
, a certain operation is repeated until a certain condition is met:
counter = 10
stop_condition = 0
while counter >= stop_condition:
print(counter)
counter -= 1
You can also enrich your while loop with an else
statement:
counter = 10
stop_condition = 0
while counter >= stop_condition:
print(counter)
counter -= 1
else:
print("BOOOOM!")
If you want to mess up your computer, run a while loop without a stop condition;) CTRL+C will save you in such situations.
While-loops are useful when you want to approximate something and repeat the approximation until the error is very small. For example, suppose you want to calculate $\sqrt2$ using an ancient method of the Babylonians,
error = 0.0001
previous_val = 1.0 * (1 + 2)
new_val = 0.5 * (1 + 2)
approximations = [previous_val, new_val]
while abs(new_val - previous_val) > error: # Continue until the error is small
previous_val = new_val
new_val = 0.5 * (new_val + 2/new_val)
approximations.append(new_val)
print("Approximation: ", new_val, "\n",
"Analytic solution: ", 2**0.5, "\n",
"Error: ", new_val-2**0.5, "\n")
Here is a plot (we will learn how to create plots lates):
%matplotlib inline
import matplotlib.pyplot as plt # used for plotting
from matplotlib.ticker import MaxNLocator
plt.clf()
fig, ax = plt.subplots()
ax.plot(range(len(approximations)), approximations, label="Approximation")
ax.plot(range(len(approximations)), [2**0.5]*len(approximations), linestyle="--", label="True value")
ax.legend(loc=1)
ax.set_xlabel("Iteration step")
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.set_ylabel("New approximation")
ax.set_title("Approximation of square root of two")
plt.show()
Python is organized with modules and packages (some of them also called 'libraries', but do not worry about these differences). This means that Python 'as is' has only limited functionality. However, many people have written modules that you can use to do more stuff.
At the moment you can think of modules as scripts that somebody has written, and that your Python distribution can access. Packages are collections of modules, which are usually used simultaneously. The precise difference is, however, not important at this stage.
For example, in base Python, there is no function to take the square root of a number, we need to write:
5**0.5
However, there is a module called math
that contains many useful function and variable definitions.
It also contains a function sqrt
.
To use it, we need to import
the module first.
Conventionally, this is done at the very beginning of your script using the keyword import
.
import math
math.sqrt(5)
To access the function sqrt
, you need to tell Python that it is in the math module first.
Therefore, you need to write math.sqrt
, and not just sqrt
.
This is because Python uses different namespaces
for the imported modules and your current file.
You can, however, also load the function into your current namespace:
import math
from math import sqrt
sqrt(5)
This can, however, can get confusing, so usually using the prefixes is a better idea.
Also, beware of using the command from math import *
, which loads everything in the math module into your current namespace. This causes even more confusion!
But the math module not only contains function definitions, but also variable definitions.
Suppose you want to use pi
or the Eulerian number
:
print(math.pi)
print(math.e)
To figure out what is actually contained in a moduls you can use the dir
function:
# dir(math) # returns a sorted list with all things defined in the math moduls
If called without input, dir
returns all variables you have defined so far in your current namespace:
# dir()
Sometimes the name of a module is inconvenient. In this case you might use an alias for this name.
For example, a very common module is called numpy
, which allows you to carry out many mathematical operations very efficiently.
To avoid writing numpy
all the time, it is now a convention to import it as np
import numpy as np
x = np.array([[1,2], [3,4]])
x
There are thousands of modules available for Python. Some of them come with the base installation, others need to be installed later. Your Anaconda distribution comes with basically all modules needed for modelling and scientific computing, so you just need to import them.
This way, the functionality of Python keeps growing, and you can be sure that it never gets 'outdated'!
It is easy to write your own modules: write some stuff in a file, save the file in your current working directory. Then it is ready for being imported. This gets useful when you want to break projects into several files. We will get back to this when we work on our first ABM!
Although pretty annoying in the beginning you will soon learn to appreciate error messages: they help you to identify mistakes you made unconsciously, and give hints on how to fix them.
Remember: if there were no error messages, the program would just do something weird, without telling you, and in the end you were left wondering why the program does not return what you want it to!
Let us now look at an error message in more detail:
x = 2
y = "Hello!"
x / y
In the first line, we see the type of the error.
In our case, we have a TypeError
: we want to divide two objects, but division is not defined for objects of the type str
.
Therefore, Python explains to us in the last line that dividing an integer by a string is not supported.
In the middle of the error message, Python tells us where the error occurs: the name of the input file, the module (more on this below), and the exact location in the code.
Often, the error messages contain enough information to resolve the error, but in other instances you must use a debugger. This helps you to delve into the code exactly before the error occurs, so you can inspect the objects in your program. For example, you check the types, lengths and contents of the objects, and trace the original source of the error.
This activity is called "debugging" and usually consumes a considerable share of your work time.
We will learn about debugging later in this course.
Sometimes it is not immediately obvious to you why the error occurs. Then you may comment out the line with the error, rerun the program, and see whether the error still persists (and whether the type of error has changed).
But for now, lets have a look at errors that occur frequently.
x = 2
y = "3"
print(x / y)
We already encountered this type of error before: a type error means that you want to use an operation on an object, for which - because of its type - this operation is not defined.
Good ways to track and understand TypeErrors is to add a print()
statement that prints the variable and its type:
print(x, type(x))
x = 3
print(4*z)
A name error means that you want python to follow a reference that does not exist. In the preceeding case, z was never assigned any value, so it is not possible to multiply it.
The best way to spot a name error is to first, check whether there is a typo in the call, and second to search your code via an editor to find the location where the variable is first defined. If this is after the first call, you have to move the assignment.
"x" = 2
A syntax error occurs when python cannot figure out the syntax of a particular statement.
In this case, it is simply impossible to assign a value to a string...if you want to, you need to use a dictionary
.
Usually, error messages are usually very informative, but sometimes they can also become misleading.
For example, 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:
# 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]
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 more complex code 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!
A useful way of handling errors and exceptions is the try/except block. This allows you to, first try whether some code works. If it does, no problem. But in case it produces a particular error Python automatically applies another block of code to correct for the error.
If the correction works, the program proceeds normally. If not, it raises an error:
x = "2" # Note that x is of type str
y = 4
try:
z = y / x
except TypeError:
print("A type error occured. Try to resolve by converting x to float.")
x = float(x)
z = y / x
print(z)
In the preceeding case, it would not be worth the effort to exit the program only because a string has not been converted to a float. But it is nevertheless useful to add the print statement so that you know that the exception has occured.
One useful application of the try/except block is when you want to make sure that variable names are free:
x = 2
y = 4
del(x) # Make sure that x does not exist and y exists
try:
del(x)
except NameError: # if x does not exist...
print("x did not exist, no need to delete it.")
pass
try:
del(y)
print("Succesfully deleted y!")
except NameError: # if y does not exist...
pass
# Now we can be sure that x and y are not associated with any other object
Note that in the original example, if x could not have been converted to a float, Python would still return a TypeError, but from within the except statement.
Also, if a different error occurs within the try block, e.g. a NameError because x has not been defined previously, Python returns this error and exits the program:
x = "a" # Note that x is of type str and cannot be converted to a float
y = 4
try:
z = y / x
except TypeError:
print("A type error occured. Try to resolve by converting x to float.")
x = float(x)
z = y / x
print(z)
Python has a very good documentation. If you are not sure about what a particular function does, check the help:
help(sum) # or "sum?"
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.
Writing your model in a way that is comprehensible to others is very important.
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.
When discussing features of code that make it particularly readable to others (but also to youreself after a while), the following points pop up frequently.
For the sake of readability, you should always try to avoid the following:
You have managed to learn a great deal of concepts today. Next week we will cover the following topics: