Week 2 — Fundamentals of Programming in Python

Programming and Numerical Methods for Economics (ECNM10115) · The University of Edinburgh

Learning goals. After this notebook you can: work with Python’s main data types (integers, floats, booleans, strings, lists, tuples, dictionaries); use NumPy arrays fluently; and control your programs with loops, conditionals, and functions.

Run every cell (Shift+Enter), attempt each Your turn before opening the solution, and predict outputs before running the Quick check cells.

import numpy as np

Indentation

Python uses indentation (4 spaces) to define blocks — where other languages use braces or end. Get it wrong and the code means something different. Here is the log-utility function economists use everywhere:

def log_u(c):
    if c < 1e-8:
        u = -np.inf        # one indent level: inside the if-block
    else:
        u = np.log(c)
    return u

print(log_u(2.0))
print(log_u(-1.0))
0.6931471805599453
-inf

Quick check ✋ — these two snippets contain the same lines. Why do they print different things?

for i in [1,2]:          for i in [1,2]:
    for j in [5,6]:          print(i)
        print(i,j)       for j in [5,6]:
                             print(j)
Answer Left: the second loop is inside the first (nested) — it prints every (i,j) combination. Right: the loops run one after the other. Only the indentation differs!

Integers and floats

The computer distinguishes integers from floats because floats carry more information while integer arithmetic is faster and exact.

y = 2       # integer
x = 2.0     # float
print([type(y), type(x)])

print(1/2)    # normal division  -> 0.5 (a float)
print(1//2)   # integer division -> 0
[<class 'int'>, <class 'float'>]
0.5
0
# Converting to float
print(float(y), float('4'), float(True))

# Not-a-number: impossible operations return nan rather than crashing
print(np.log(-1))

# Handy built-ins
a = 2.4
print(round(a), abs(-23))
2.0 4.0 1.0
nan
2 23
/tmp/ipykernel_4010/103602420.py:5: RuntimeWarning: invalid value encountered in log
  print(np.log(-1))

Augmented assignment

x += 1 is shorthand for x = x + 1. You will use this constantly to accumulate results.

x = 3
x += 1
print(x)    # 4

x = 3
x *= 2
print(x)    # 6

x = 3
x /= 2
print(x)    # 1.5
4
6
1.5

Booleans

Booleans are a numeric type in Python: True behaves like 1 and False like 0.

x = True
y = 100 < 0          # comparisons produce booleans
print(type(y), y)

print(x + y, x * y)   # arithmetic with booleans

bools = [True, True, False, True]
print(sum(bools))     # counts the Trues!

print(bool(3))        # bool(x) is equivalent to x != 0
<class 'bool'> False
1 0
3
True
# Compound boolean logic
x, z, y = True, 100 > 10, 100 < 0
print(x and (z or y))    # True
print((x or z) and y)    # False

a, b, c = 5, 1, 'you'
print(b > 0 and a < 10 or len(c) > 3)
print(b > 0 and a < 10 and len(c) > 3)
True
False
True
False

Strings

str1 = 'This is a string in Python'
str2 = str(4.29)              # convert a number to a string

print(str1[0])       # 'T' — strings are indexed like lists
print(str1[10:16])   # slicing: 'string'

# Strings are IMMUTABLE — you cannot change them in place:
try:
    str1[9] = 'a'
except TypeError as e:
    print('Error:', e)
T
string
Error: 'str' object does not support item assignment
# String operators
a = 'black'
b = 'pepper'

print(a + b)        # concatenation
print(3 * b)        # repetition
print('x' in b)     # membership test
blackpepper
pepperpepperpepper
False

Lists

Lists are Python’s workhorse container: ordered, sliceable, and mutable — and they can hold anything, even functions.

y = lambda x: x**2                    # a function object
list_obj = [1, 2, 3, 'a', 'b', y]     # numbers, strings, and a function in one list
list_obj2 = [[1, 2, 3], ['a', 'b']]   # a list of lists

print(list_obj[0])        # indexing
print(list_obj[1:3])      # slicing
print(list_obj2[0][0])    # first element of the first inner list
1
[2, 3]
1
list_1 = [1, 2, 3]

list_1.append(4)          # add one element at the end
print(list_1)

list_1.remove(2)          # remove an element
print(list_1)

list_1.append([4, 5, 6])  # append adds the WHOLE list as one element
print(list_1)
list_1.remove([4, 5, 6])

list_1.extend([4, 5, 6])  # extend adds each element separately
print(list_1)

list_1.reverse()
print(list_1)

print(list_1.index(5))    # where is 5?
[1, 2, 3, 4]
[1, 3, 4]
[1, 3, 4, [4, 5, 6]]
[1, 3, 4, 4, 5, 6]
[6, 5, 4, 4, 3, 1]
1

Your turn 2.1 — Start from prices = [101.2, 99.8, 103.4]. Append 105.1, then compute the list of returns \(r_t = p_t/p_{t-1} - 1\) using a loop, and print it.

Predict first: how many returns should a list of 4 prices produce?

prices = [101.2, 99.8, 103.4]

# Your attempt here:
💡 Click to reveal the solution — but try it yourself first!
prices.append(105.1)

returns = []
for t in range(1, len(prices)):
    r = prices[t] / prices[t-1] - 1
    returns.append(r)
print(returns)
3 returns from 4 prices — one fewer, since returns need a previous price.

NumPy arrays

Arrays are how we do serious numerical work. Unlike lists, they support fast element-wise maths.

flat_a = np.array([1, 2, 3, 4, 5, 6])   # 1-D (flat) array
print(flat_a[0], flat_a[3:])

a = np.array([[1, 2, 3], [4, 5, 6]])    # 2-D array (a matrix)
print(a)
print(a.shape)      # (rows, columns)
print(a[0, :])      # first row, all columns
print(a[:, 1])      # second column
1 [4 5 6]
[[1 2 3]
 [4 5 6]]
(2, 3)
[1 2 3]
[2 5]
# Standard arrays you will create all the time
zeros = np.zeros(10)
print(zeros.reshape((2, 5)))     # reshape returns a NEW array...
zeros = zeros.reshape((2, 5))    # ...assign it to keep the change

m1 = np.ones((3, 4))             # 3x4 matrix of ones
b  = np.eye(4)                   # identity matrix
x  = np.linspace(0, 99, 100)     # 100 evenly spaced points from 0 to 99
print(np.linspace(2, 4, 5))
[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]
[2.  2.5 3.  3.5 4. ]
# Array methods
print(x.sum(), x.min(), x.mean(), x.max())
print(x.argmax())        # INDEX of the maximum element

a = np.array([[1, 2, 3], [4, 5, 6]])

# Operations along axes: axis=0 works column-wise, axis=1 row-wise
print(a.sum())           # all elements
print(a.sum(axis=0))     # per column
print(a.sum(axis=1))     # per row
print(a.mean(axis=0))

# NumPy functions apply element-wise
print(np.log(a))
4950.0 0.0 49.5 99.0
99
21
[5 7 9]
[ 6 15]
[2.5 3.5 4.5]
[[0.         0.69314718 1.09861229]
 [1.38629436 1.60943791 1.79175947]]
# Comparisons are element-wise too
print(a > 3)
print((a > 3).any(), (a > 3).all())

# argmax on a matrix returns the position in the FLATTENED array —
# use unravel_index to convert it to (row, column):
idx = a.argmax()
i_row, i_col = np.unravel_index(idx, a.shape)
print(a[i_row, i_col])   # the maximum element

print(a.T)               # transpose
[[False False False]
 [ True  True  True]]
True False
6
[[1 4]
 [2 5]
 [3 6]]

Your turn 2.2 — Create the matrix M = np.array([[3., 7., 1.], [9., 2., 5.]]). Find: (a) the column-wise means, (b) the position (row, column) of the largest element, (c) how many elements are greater than 4.

# Your attempt here:
💡 Click to reveal the solution — but try it yourself first!
M = np.array([[3., 7., 1.], [9., 2., 5.]])

print(M.mean(axis=0))                           # (a)
i, j = np.unravel_index(M.argmax(), M.shape)
print((i, j))                                   # (b) -> (1, 0)
print((M > 4).sum())                            # (c) booleans sum as 1/0 -> 3

Tuples

Tuples look like lists but are immutable — perfect for things that shouldn’t change, and for functions that return several values.

y = ('a', 'b')
z = 4, 5                 # parentheses are optional
print(y[0])

try:
    y[0] = 10            # tuples are immutable
except TypeError as e:
    print('Error:', e)

z1, z2 = z               # unpacking (as with unravel_index above)
print(z1, z2)
a
Error: 'tuple' object does not support item assignment
4 5

Dictionaries

Dictionaries map keys to values — ideal for keeping model parameters tidy.

individual = {'name': 'Francis', 'age': 28, 'weight': 100}
print(individual['age'])
print(individual['weight'])

# A very common economics pattern: a parameter dictionary
params = {'alpha': 0.3, 'beta': 0.96, 'delta': 0.05}
print(params['beta'])
28
100
0.96

Loops

# Accumulate results by appending to a list
squares = []
for i in range(10):
    squares.append(i**2)
print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Loop directly over items — and use enumerate when you also need the index
list_verbs = ['run', 'sit', 'jump', 'play', 'dance']

for i, verb in enumerate(list_verbs):
    print('Verb ' + str(i+1) + ': ' + verb)    # indexing starts at 0, so +1

# zip loops over two lists in tandem
list_verbs_spanish = ['correr', 'sentarse', 'saltar', 'jugar', 'bailar']
for v_eng, v_spa in zip(list_verbs, list_verbs_spanish):
    print(v_eng + ': ' + v_spa)
Verb 1: run
Verb 2: sit
Verb 3: jump
Verb 4: play
Verb 5: dance
run: correr
sit: sentarse
jump: saltar
play: jugar
dance: bailar
# Nested loops: the inner loop runs fully for each step of the outer loop
for i in range(6, 8):
    print('Multiplication table of', i)
    for j in range(1, 11):
        print(i, "*", j, "=", i*j)
Multiplication table of 6
6 * 1 = 6
6 * 2 = 12
6 * 3 = 18
6 * 4 = 24
6 * 5 = 30
6 * 6 = 36
6 * 7 = 42
6 * 8 = 48
6 * 9 = 54
6 * 10 = 60
Multiplication table of 7
7 * 1 = 7
7 * 2 = 14
7 * 3 = 21
7 * 4 = 28
7 * 5 = 35
7 * 6 = 42
7 * 7 = 49
7 * 8 = 56
7 * 9 = 63
7 * 10 = 70
# While loops: repeat until a condition fails
count = 1
while count < 5:
    print(count)
    count += 1
1
2
3
4
# While loops shine when you don't know in advance how many steps you need —
# here: searching for the solution of 3 = x/2, starting from a guess
LHS = 3
eps = 0.001
x = 20
RHS = x / 2
count = 1
while np.abs(LHS - RHS) > eps:
    x -= 1
    RHS = x / 2
    count += 1
    if np.abs(LHS - RHS) < eps:
        print('solution is x =', x)
    elif count == 20:
        print('no solution found with', count, 'iterations')
        break
    else:
        continue
solution is x = 6
Quick check ✋ — change x = 20 to x = 100 above and re-run. Why does it fail to find the solution?
Answer Starting from 100 and stepping down by 1, after 20 iterations x is only at ~80 — far from the solution x=6 — and the count==20 guard stops the search. Iterative algorithms need either enough iterations or a smarter update rule. This idea — convergence and iteration limits — returns in every numerical method later in the course.

Conditional statements

import random
x = random.random()
print('Random number is x =', x)
if x > 0.3 and x < 0.5:
    print("You win a prize of $1, congratulations!")
else:
    print("Sorry, you win nothing!")
Random number is x = 0.1890908435474722
Sorry, you win nothing!
# Combining while + if/else: classify numbers as even or odd
n = 7
while n > 0:
    if n % 2 == 0:
        print(n, 'is an even number')
    else:
        print(n, 'is an odd number')
    n = n - 1
7 is an odd number
6 is an even number
5 is an odd number
4 is an even number
3 is an odd number
2 is an even number
1 is an odd number

Functions

def say_hi(name):
    return 'Hi ' + name + '! We welcome you at the econ-programming course in the UoE.'

print(say_hi('Brad Pitt'))
Hi Brad Pitt! We welcome you at the econ-programming course in the UoE.
# Positional vs keyword arguments — keyword arguments have default values
def f(x, y, a=2, b=2):
    return x**a + y*b

print(f(2, 4))              # use both defaults
print(f(2, 4, b=6))         # override one
print(f(2, 4, a=6, b=3))    # override both
12
28
76

Your turn 2.3 — Write a function ces(K, L, alpha=0.5, sigma=2.0, A=1.0) implementing the CES production function

\[Y = A\left(\alpha K^{\frac{\sigma-1}{\sigma}} + (1-\alpha)L^{\frac{\sigma-1}{\sigma}}\right)^{\frac{\sigma}{\sigma-1}}\]

and evaluate it at K=4, L=9. (This function stars in PS1 and this week’s lab.)

# Your attempt here:
💡 Click to reveal the solution — but try it yourself first!
def ces(K, L, alpha=0.5, sigma=2.0, A=1.0):
    rho = (sigma - 1) / sigma
    return A * (alpha * K**rho + (1 - alpha) * L**rho)**(1 / rho)

print(ces(4, 9))
Expected output: about 6.25.

Self-check quiz

Q1. What is 1//2?
Answer 0 — integer division truncates.
Q2. sum([True, False, True, True]) = ?
Answer 3 — booleans are numeric.
Q3. Which of these can you modify in place: a list, a tuple, a string?
Answer Only the list. Tuples and strings are immutable.
Q4. For matrix a, what does a.sum(axis=0) compute?
Answer Column sums (axis 0 runs down the rows).

Before the lab

  • Finish Your turn 2.1–2.3 — the CES function is directly relevant to PS1.
  • The lab gives tips for PS1 Exercises 1–2; review the Python refresher sections it points to.
  • Reference: QuantEcon — Python Essentials.

Next week: data manipulation and data analysis with pandas.