Coding Basics#

In this chapter, you’ll learn about the basics of objects, types, operations, conditions, loops, functions, and imports. These are the basic building blocks of almost all programming languages and will serve you well for your coding and economics journey.

This chapter has benefited from the excellent Python Programming for Data Science book by Tomas Beuzen.


Remember, you can launch this page interactively by using the ‘Colab’ button under the rocket symbol () at the top of the page. You can also download this page as a Jupyter Notebook to run on your own computer: use the ‘download .ipynb’ button under the download symbol the top of the page and open that file using Visual Studio Code.

If you get stuck#

It’s worth saying at the outset that no-one memorises half of the stuff you’ll see in this book. 80% or more of time spent programming is actually time spent looking up how to do this or that online, ‘debugging’ a code for errors, or testing code. This applies to all programmers, regardless of level. You are here to learn the skills and concepts of programming, not the precise syntax (which is easy to look up later).


Knowing how to Google is one of the most important skills of any coder. No-one remembers every function from every library. Here are some useful coding resources:

  • when you have an error, look on Stack Overflow to see if anyone else had the same error (they probably did) and how they overcame it.

  • if you’re having trouble navigating a new package or library, look up the documentation online. The best libraries put as much effort into documentation as they do the code base.

  • use cheat sheets to get on top of a range of functionality quickly. For instance, this excellent (mostly) base Python Cheat Sheet.

  • if you’re having a coding issue, take a walk to think about the problem, or explain your problem to an animal toy on your desk (traditionally a rubber duck, but other animals are available).

Coding Basics#

Let’s review some basics in the interests of getting you up to speed as quickly as possible. You can use Python as a calculator:

print(1 / 200 * 30)
print((59 + 73 + 2) / 3)

The extra package numpy contains many of the additional mathematical operators that you might need. If you don’t already have numpy installed, open up the terminal in Visual Studio Code (go to “Terminal -> New Terminal” and then type pip install numpy into the terminal then hit return). Once you have numpy installed, you can import it and use it like this:

import numpy as np

print(np.sin(np.pi / 2))

You can create new objects with the assignment operator =. You should think of this as copying the value of whatever is on the right-hand side into the variable on the left-hand side.

x = 3 * 4

There are several structures in Python that capture multiple objects simultaneously but perhaps the most common is the list, which is designated by square brackets.

primes = [1, 2, 3, 5, 7, 11, 13]
[1, 2, 3, 5, 7, 11, 13]

All Python statements where you create objects (known as assignment statements) have the same form:

object_name = value

When reading that code, say “object name gets value” in your head.


Python will ignore any text after #. This allows to you to write comments, text that is ignored by Python but can be read by other humans. We’ll sometimes include comments in examples explaining what’s happening with the code.

Comments can be helpful for briefly describing what the subsequent code does.

# define primes
primes = [1, 2, 3, 5, 7, 11, 13]
# multiply primes by 2
[el * 2 for el in primes]
[2, 4, 6, 10, 14, 22, 26]

With short pieces of code like this, it is not necessary to leave a command for every single line of code and you should try to use informative names wherever you can because these help readers of your code (likely to be you in the future) understand what is going on!

Keeping Track of Variables#

You can always inspect an already-created object by typing its name into the interactive window:

[1, 2, 3, 5, 7, 11, 13]

If you want to know what type of object it is, use type(object) in the interactive window like this:


Visual Studio Code has some powerful features to help you keep track of objects:

  1. At the top of your interactive window, you should see a ‘Variables’ button. Click it to see a panel appear with all variables that you’ve defined.

  2. Hover your mouse over variables you’ve previously entered into the interactive window; you will see a pop-up that tells you what type of object it is.

  3. If you start typing a variable name into the interactive window, Visual Studio Code will try to auto-complete the name for you. Press the ‘tab’ key on your keyboard to accept the top option.

Calling Functions#

If you’re an economist, you hardly need to be told you what a function is. In coding, it’s much the same as in mathematics: a function has inputs, it performs its function, and it returns any outputs. Python has a large number of built-in functions. You can also import functions from packages (like we did with np.sin) or define your own.

In coding, a function has inputs, it performs its function, and it returns any outputs. Let’s see a simple example of using a built-in function, sum():


The general structure of functions is the function name, followed by brackets, followed by one or more arguments. Sometimes there will also be keyword arguments. For example, sum() comes with a keyword argument that tells the function to start counting from a specific number. Let’s see this in action by starting from ten:

sum(primes, start=10)

If you’re ever unsure of what a function does, you can call help() on it (itself a function):

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.

Or, in Visual Studio Code, hover your mouse over the function name.


Why does this code not work?

my_variable = 10

Look carefully! This may seem like an exercise in pointlessness, but training your brain to notice even the tiniest difference will pay off when programming.

Values, variables, and types#

A value is datum such as a number or text. There are different types of values: 352.3 is known as a float or double, 22 is an integer, and “Hello World!” is a string. A variable is a name that refers to a value: you can think of a variable as a box that has a value, or multiple values, packed inside it.

Almost any word can be a variable name as long as it starts with a letter or an underscore, although there are some special keywords that can’t be used because they already have a role in the Python language: these include if, while, class, and lambda.

Creating a variable in Python is achieved via an assignment (putting a value in the box), and this assignment is done via the = operator. The box, or variable, goes on the left while the value we wish to store appears on the right. It’s simpler than it sounds:

a = 10

This creates a variable a, assigns the value 10 to it, and prints it. Sometimes you will hear variables referred to as objects. Everything that is not a literal value, such as 10, is an object. In the above example, a is an object that has been assigned the value 10.

How about this:

b = "This is a string"
This is a string

It’s the same thing but with a different type of data, a string instead of an integer. Python is dynamically typed, which means it will guess what type of variable you’re creating as you create it. This has pros and cons, with the main pro being that it makes for more concise code.


Everything is an object, and every object has a type.

The most basic built-in data types that you’ll need to know about are: integers 10, floats 1.23, strings like this, booleans True, and nothing None. Python also has a built-in type called a list [10, 15, 20] that can contain anything, even different types. So

list_example = [10, 1.23, "like this", True, None]
[10, 1.23, 'like this', True, None]

is completely valid code. None is a special type of nothingness, and represents an object with no value. It has type NoneType and is more useful than you might think!

As well as the built-in types, packages can define their own custom types. If you ever want to check the type of a Python variable, you can call the type() function on it like so:


This is especially useful for debugging ValueError messages.

Below is a table of common data types in Python:


Type name

Type Category





Numeric Type

positive/negative whole numbers


floating point number


Numeric Type

real number in decimal form




Boolean Values

true or false




Sequence Type


"Hello World!"



Sequence Type

a collection of objects - mutable & ordered

['text entry', True, 16]



Sequence Type

a collection of objects - immutable & ordered

(51.02, -0.98)



Mapping Type

mapping of key-value pairs

{'name':'Ada', 'subject':'computer science'}



Null Object

represents no value





Represents a function

def add_one(x): return x+1


What type is this Python object?

cities_to_temps = {"Paris": 32, "London": 22, "Seville": 36, "Wellesley": 29}

What type is the first key (hint: comma separated entries form key-value pairs)?


You may notice that there are several kinds of brackets that appear in the code we’ve seen so far, including [], {}, and (). These can play different roles depending on the context, but the most common uses are:

  • [] is used to denote a list, eg ['a', 'b'], or to signify accessing a position using an index, eg vector[0] to get the first entry of a variable called vector.

  • {} is used to denote a set, eg {'a', 'b'}, or a dictionary (with pairs of terms), eg {'first_letter': 'a', 'second_letter': 'b'}.

  • () is used to denote a tuple, eg ('a', 'b'), or the arguments to a function, eg function(x) where x is the input passed to the function, or to indicate the order operations are carried out.

Lists and slicing#

Lists are a really useful way to work with lots of data at once. They’re defined with square brackets, with entries separated by commas. You can also construct them by appending entries:

list_example.append("one more entry")
[10, 1.23, 'like this', True, None, 'one more entry']

And you can access earlier entries using an index, which begins at 0 and ends at one less than the length of the list (this is the convention in many programming languages). For instance, to print specific entries at the start, using 0, and end, using -1:

one more entry


How might you access the penultimate entry in a list object if you didn’t know how many elements it had?

As well as accessing positions in lists using indexing, you can use slices on lists. This uses the colon character, :, to stand in for ‘from the beginning’ or ‘until the end’ (when only appearing once). For instance, to print just the last two entries, we would use the index -2: to mean from the second-to-last onwards. Here are two distinct examples: getting the first three and last three entries to be successively printed:

[10, 1.23, 'like this']
[True, None, 'one more entry']

Slicing can be even more elaborate than that because we can jump entries using a second colon. Here’s a full example that begins at the second entry (remember the index starts at 0), runs up until the second-to-last entry (exclusive), and jumps every other entry inbetween (range just produces a list of integers from the value to one less than the last):

list_of_numbers = list(range(1, 11))
start = 1
stop = -1
step = 2
[2, 4, 6, 8]

A handy trick is that you can print a reversed list entirely using double colons:

[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]


Slice the list_example from earlier to get only the first five entries.

As noted, lists can hold any type, including other lists! Here’s a valid example of a list that’s got a lot going on:

wacky_list = [
    ["five", 4, 3],
    (91, 93, 90),
    "Hello World!",
    {"key": "value", "key2": "value2"},
 ['five', 4, 3],
 (91, 93, 90),
 'Hello World!',
 {'key': 'value', 'key2': 'value2'}]

In reality, it’s usually not a good idea to mix data types in a list, but Python is very flexible. Other iterables (objects composed of multiple elements, of which the list is just one in Python) can also store objects of different types.


Can you identify the types of each of the entries in wacky_list?


All of the basic operators you see in mathematics are available to use: + for addition, - for subtraction, * for multiplication, ** for powers, / for division, and % for modulo. These work as you’d expect on numbers. But these operators are sometimes defined for other built-in data types too. For instance, we can ‘sum’ strings (which really concatenates them):

string_one = "This is an example "
string_two = "of string concatenation"
string_full = string_one + string_two
This is an example of string concatenation

It works for lists too:

list_one = ["apples", "oranges"]
list_two = ["pears", "satsumas"]
list_full = list_one + list_two
['apples', 'oranges', 'pears', 'satsumas']

Perhaps more surprisingly, you can multiply strings!

string = "apples, "
print(string * 3)
apples, apples, apples, 

Below is a table of the basic arithmetic operations.














integer division / floor division




matrix multiplication

As well as the usual operators, Python supports assignment operators. An example of one is x+=3, which is equivalent to running x = x + 3. Pretty much all of the operators can be used in this way.


Using Python operations only, what is

\[ \frac{2^5}{7 \cdot (4 - 2^3)} \]


In some ways, strings are treated a bit like lists, meaning you can access the individual characters via slicing and indexing. For example:

string = "cheesecake"

Both lists and strings will also allow you to use the len() command to get their length:

string = "cheesecake"
print("String has length:")
list_of_numbers = range(1, 20)
print("List of numbers has length:")
String has length:
List of numbers has length:


What is the len of a list created by range(n) where n could be any integer?

Strings have type string and can be defined by single or double quotes, eg string = "cheesecake" would have been equally valid above. It’s best practice to use one convention and stick to it, and most people use double quotes for strings.

There are various functions built into Python to help you work with strings that are particularly useful for cleaning messy data. For example, imagine you have a variable name like ‘This Is /A Variable ‘. (You may think this is implausibly bad; if only that were true…). Let’s see if we can clean this up:

string = "This Is /A Variable   "
string = string.replace("/", "").rstrip().lower()
this is a variable

The steps above replace the character ‘/’, strip out whitespace on the right hand-side of the string, and put everything in lower case. The brackets after the words signify that a function has been applied; we’ll see more of functions later.


Using string operations, strip the leading and trailing spaces, make upper case, and remove the underscores from the string "    this_is_a_better_variable_name   ".

Changing Type to String

We’ll look at this in more detail shortly, but while we’re on strings, it seems useful to mention it now: you’ll often want to output one type of data as another, and Python generally knows what you’re trying to achieve if you, for example, print() a boolean value. For numbers, there are more options and you can see a big list of advice on string formatting of all kinds of things here. For now, let’s just see a simple example of something called an f-string, a string that combines a number and a string (these begin with an f for formatting):

value = 20
sqrt_val = 20 ** 0.5
print(f"The square root of {value:d} is {sqrt_val:.2f}")
The square root of 20 is 4.47

The formatting command :d is an instruction to treat value like an integer, while :.2f is an instruction to print it like a float with 2 decimal places.


f-strings are only available in Python 3.6+


Write a print command with the sqrt_val expressed to 3 decimal places.

Booleans and conditions#

Some of the most important operations you will perform are with True and False values, also known as boolean data types. There are two types of operation that are associated with booleans: boolean operations, in which existing booleans are combined, and condition operations, which create a boolean when executed.

Boolean operators that return booleans are as follows:



x and y

are x and y both True?

x or y

is at least one of x and y True?

not x

is x False?

These behave as you’d expect: True and False evaluates to False, while True or False evaluates to True. There’s also the not keyword. For example

not True

as you would expect.

Conditions are expressions that evaluate as booleans. A simple example is 10 == 20. The == is an operator that compares the objects on either side and returns True if they have the same values–though be careful using it with different data types.

Here’s a table of conditions that return booleans:



x == y

is x equal to y?

x != y

is x not equal to y?

x > y

is x greater than y?

x >= y

is x greater than or equal to y?

x < y

is x less than y?

x <= y

is x less than or equal to y?

x is y

is x the same object as y?

As you can see from the table, the opposite of == is !=, which you can read as ‘not equal to the value of’. Here’s an example of ==:

boolean_condition = 10 == 20


What does not (not True) evaluate to?

The real power of conditions comes when we start to use them in more complex examples. Some of the keywords that evaluate conditions are if, else, and, or, in, not, and is. Here’s an example showing how some of these conditional keywords work:

name = "Ada"
score = 99

if name == "Ada" and score > 90:
    print("Ada, you achieved a high score.")

if name == "Smith" or score > 90:
    print("You could be called Smith or have a high score")

if name != "Smith" and score > 90:
    print("You are not called Smith and you have a high score")
Ada, you achieved a high score.
You could be called Smith or have a high score
You are not called Smith and you have a high score

All three of these conditions evaluate as True, and so all three messages get printed. Given that == and != test for equality and not equal, respectively, you may be wondering what the keywords is and not are for. Remember that everything in Python is an object, and that values can be assigned to objects. == and != compare values, while is and not compare objects. For example,

name_list = ["Ada", "Adam"]
name_list_two = ["Ada", "Adam"]

# Compare values
print(name_list == name_list_two)

# Compare objects
print(name_list is name_list_two)

Note that code with lots of branching if statements is not very helpful to you or to anyone else who reads your code. Some automatic code checkers will pick this up and tell you that your code is too complex. Almost all of the time, there’s a way to rewrite your code without lots of branching logic that will be better and clearer than having many nested if statements.

One of the most useful conditional keywords is in. This one must pop up ten times a day in most coders’ lives because it can pick out a variable or make sure something is where it’s supposed to be.

name_list = ["Lovelace", "Smith", "Hopper", "Babbage"]

print("Lovelace" in name_list)

print("Bob" in name_list)


Check if “a” is in the string “Walloping weasels” using in. Is “a” in “Anodyne”?

The opposite is not in.

Finally, one conditional construct you’re bound to use at some point, is the ifelse structure:

score = 98

if score == 100:
    print("Top marks!")
elif score > 90 and score < 100:
    print("High score!")
elif score > 10 and score <= 90:
    print("Better luck next time.")
High score!

Note that this does nothing if the score is between 11 and 90, and prints a message otherwise.


Create a new ifelifelse statement that prints “well done” if a score is over 90, “good” if between 40 and 90, and “bad luck” otherwise.

One nice feature of Python is that you can make multiple boolean comparisons in a single line.

a, b = 3, 6

1 < a < b < 20


You’ll have seen that certain parts of the code examples are indented. Code that is part of a function, a conditional clause, or loop is indented. This isn’t a code style choice, it’s actually what tells the language that some code is to be executed as part of, say, a loop and not to executed after the loop is finished.

Here’s a basic example of indentation as part of an if loop. The print() statement that is indented only executes if the condition evaluates to true.

x = 10

if x > 2:
    print("x is greater than 2")
x is greater than 2


The VS Code extension indent-rainbow colours different levels of indentation differently for ease of reading.

When functions, conditional clauses, or loops are combined together, they each cause an increase in the level of indentation. Here’s a double indent.

if x > 2:
    print("outer conditional cause")
    for i in range(4):
        print("inner loop")
outer conditional cause
inner loop
inner loop
inner loop
inner loop

The standard practice for indentation is that each sub-statement should be indented by 4 spaces. It can be hard to keep track of these but, as usual, Visual Studio Code has you covered. Go to Settings (the cog in the bottom left-hand corner, then click Settings) and type ‘Whitespace’ into the search bar. Under ‘Editor: Render Whitespace’, select ‘boundary’. This will show any whitespace that is more than one character long using faint grey dots. Each level of indentation in your Python code should now begin with four grey dots showing that it consists of four spaces.


Rendering whitespace using Visual Studio Code’s settings makes it easier to navigate different levels of indentation.


Try writing a code snippet that reaches the triple level of indentation.


Another built-in Python type that is enormously useful is the dictionary. This provides a mapping one set of variables to another (either one-to-one or many-to-one). Let’s see an example of defining a dictionary and using it:

fruit_dict = {
    "Jazz": "Apple",
    "Owari": "Satsuma",
    "Seto": "Satsuma",
    "Pink Lady": "Apple",

# Add an entry
fruit_dict.update({"Cox": "Apple"})

variety_list = ["Jazz", "Jazz", "Seto", "Cox"]

fruit_list = [fruit_dict[x] for x in variety_list]
['Apple', 'Apple', 'Satsuma', 'Apple']

From an input list of varieties, we get an output list of their associated fruits. Another good trick to know with dictionaries is that you can iterate through their keys and values:

for key, value in fruit_dict.items():
    print(key + " maps into " + value)
Jazz maps into Apple
Owari maps into Satsuma
Seto maps into Satsuma
Pink Lady maps into Apple
Cox maps into Apple


Update the fruit dictionary with another two entries and then iterate through all of the entries printing each mapping using .items() as above.

Loops and list comprehensions#

A loop is a way of executing a similar piece of code over and over in a similar way. The most useful loops are for loops and list comprehensions.

A for loop does something for the time that the condition is satisfied. For example,

name_list = ["Lovelace", "Smith", "Pigou", "Babbage"]

for name in name_list:

prints out a name until all names have been printed out. Note the colon after the statement and before the indent.

As long as your object is an iterable (ie you can iterate over it), then it can be used in this way in a for loop. The most common examples are lists and tuples, but you can also iterate over strings (in which case each character is selected in turn). One gotcha to be aware of is if you iterate over a string, say “hello”, instead of iterating over a list (or tuple) of strings, eg ["hello"]. In the latter case, you get:

for entry in ["hello"]:
    print("---end entry---")
---end entry---

While in the former you get something quite different and typically not all that useful:

for entry in "hello":
    print("---end entry---")
---end entry---
---end entry---
---end entry---
---end entry---
---end entry---


Write a for loop that prints out “coding for economists” so that each word is printed in a successive iteration.

A useful trick with for loops is the enumerate keyword, which runs through an index that keeps track of the place of items in a list:

name_list = ["Lovelace", "Smith", "Hopper", "Babbage"]

for i, name in enumerate(name_list):
    print(f"The name in position {i} is {name}")
The name in position 0 is Lovelace
The name in position 1 is Smith
The name in position 2 is Hopper
The name in position 3 is Babbage

Remember, Python indexes from 0 so the first entry of i will be zero. But, if you’d like to index from a different number, you can:

for i, name in enumerate(name_list, start=1):
    print(f"The name in position {i} is {name}")
The name in position 1 is Lovelace
The name in position 2 is Smith
The name in position 3 is Hopper
The name in position 4 is Babbage

Another useful pattern when doing for loops with dictionaries is iteration over key, value pairs. As we saw earlier, what distinguishes a dictionary in Python is that it maps a key to a value, for example “apple” might map to “fruit”. Let’s take our example from earlier that mapped cities to temperatures. If we wanted to iterate over both keys and values, we can write a for loop like this:

cities_to_temps = {"Paris": 28, "London": 22, "Seville": 36, "Wellesley": 29}

for key, value in cities_to_temps.items():
    print(f"In {key}, the temperature is {value} degrees C today.")
In Paris, the temperature is 28 degrees C today.
In London, the temperature is 22 degrees C today.
In Seville, the temperature is 36 degrees C today.
In Wellesley, the temperature is 29 degrees C today.

Note that we added .items() to the end of the dictionary. And note that we didn’t have to call the key key, or the value value: these are set by their position. But part of best practice in writing code is that there should be no surprises, and writing key, value makes it really clear that you’re using values from a dictionary.


Write a dictionary that maps four cities you know into their respective countries and print the results using the key, value iteration trick.

Another useful type of for loop is provided by the zip() function. You can think of the zip() function as being like a zipper, bringing elements from two different iterators together in turn. Here’s an example:

first_names = ["Ada", "Adam", "Grace", "Charles"]
last_names = ["Lovelace", "Smith", "Hopper", "Babbage"]

for forename, surname in zip(first_names, last_names):
    print(f"{forename} {surname}")
Ada Lovelace
Adam Smith
Grace Hopper
Charles Babbage

The zip function is super useful in practice.


Zip together the first names from above with this jumbled list of surnames: ['Babbage', 'Hopper', 'Smith', 'Lovelace'].

(Hint: you have seen a trick to help re-arrange lists earlier on in the Chapter.)

List (and Other) Comprehensions

There’s a second way to do loops in Python and, in most but not all cases, they run faster. More importantly, and this is the reason it’s good practice to use them where possible, they are very readable. They are called list comprehensions.

List comprehensions can combine what a for loop and (if needed) what a condition do in a single line of code. First, let’s look at a for loop that adds one to each value done as a list comprehension (NB: in practice, we would use super-fast numpy arrays for this kind of operation):

num_list = range(50, 60)
[1 + num for num in num_list]
[51, 52, 53, 54, 55, 56, 57, 58, 59, 60]

The general pattern is a bit similar to with the for loop but there are some differences. There’s no colon, and no indenting. The syntax is “do something with x” then for x in iterable. Finally, the expression is wrapped in a [ and ] to make the output a list.

Note that lists are not the only wrapping you can provide to this kind of structure. A ( and ) to make it a generator (don’t worry about what this is for now), a { and } to make it a set (an object that only contains unique values), or it’s possible to create a dictionary from a comprehension too! List comprehensions are the most common, so if you only remember one kind, remember them.


Create a list comprehension that multiplies numbers in the range from 1 to 10 by 5.

Did you get the range right?

Let’s now see how to include a condition within a list comprehension. Say we had a list of numbers and wanted to filter it according to whether the numbers divided by 3 or not using the modulo operator:

number_list = range(1, 40)
divide_list = [x for x in number_list if x % 3 == 0]
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39]

The syntax here is do something to x for x in something if x satisfies some condition.

Here’s another example that picks out only the names that include ‘Smith’ in them:

names_list = ["Joe Bloggs", "Adam Smith", "Sandra Noone", "leonara smith"]
smith_list = [x for x in names_list if "smith" in x.lower()]
['Adam Smith', 'leonara smith']

Note how we used ‘smith’ rather than ‘Smith’ and then used lower() to ensure we matched names regardless of the case they are written in.

We can even do a whole ifelse construct inside a list comprehension:

names_list = ["Joe Bloggs", "Adam Smith", "Sandra Noone", "leonara smith"]
smith_list = [x if "smith" in x.lower() else "Not Smith!" for x in names_list]
['Not Smith!', 'Adam Smith', 'Not Smith!', 'leonara smith']

Many of the constructs we’ve seen can be combined. For instance, there is no reason why we can’t have a nested or repeated list comprehension using zip(), and, perhaps more surprisingly, sometimes these are useful!

first_names = ["Ada", "Adam", "Grace", "Charles"]
last_names = ["Lovelace", "Smith", "Hopper", "Babbage"]
names_list = [x + " " + y for x, y in zip(first_names, last_names)]
['Ada Lovelace', 'Adam Smith', 'Grace Hopper', 'Charles Babbage']

An even more extreme use of list comprehensions can deliver nested structures:

first_names = ["Ada", "Adam"]
last_names = ["Lovelace", "Smith"]
names_list = [[x + " " + y for x in first_names] for y in last_names]
[['Ada Lovelace', 'Adam Lovelace'], ['Ada Smith', 'Adam Smith']]

This gives a nested structure that (in this case) iterates over first_names first, and then last_names. (Note that this object is a list of lists of strings!)

Let’s see a dictionary comprehension now. These look a bit similar to set comprehensions because they use { and } at either end but they are different because they come with a colon separating the keys from the values:

{key: value for key, value in zip(first_names, last_names)}
{'Ada': 'Lovelace', 'Adam': 'Smith'}


Create a nested list comprehension that results in a list of lists of strings equal to [['a0', 'b0', 'c0'], ['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']] (ie a combination of the first three integers and letters of the alphabet). You may find that you need to convert numbers to strings using str(x) to do this.

If you’d like to learn more about list comprehensions, check out these short video tutorials.

Writing Functions#

Declaring a function starts with a def keyword for ‘define a function’. It then has a name, followed by brackets, (), which may contain function arguments and function keyword arguments. This is followed by a colon. The body of the function is then indented relative to the left-most text. Function arguments are defined in brackets following the name, with different inputs separated by commas. Any outputs are given with the return keyword, again with different variables separated by commas.

Arguments and keyword arguments

arguments are the variables that functions always need, so a and b in def add(a, b): return a + b. The function won’t work without them! Function arguments are sometimes referred to as args.

Keyword arguments are the variables that are optional for functions, so c in def add(a, b, c=5): return a + b - c. If you do not provide a value for c when calling the function, it will automatically revert to c=5. Keyword arguments are sometimes referred to as kwargs.

Let’s see a very simple example of a function with a single argument (or arg):

def welcome_message(name):
    return f"Hello {name}, and welcome!"

# Without indentation, this code is not part of function
name = "Ada"
output_string = welcome_message(name)
Hello Ada, and welcome!

One powerful feature of functions is that we can define defaults for the input arguments. These are called keyword arguments (or kwargs). Let’s see that in action by defining a default value for name, along with multiple outputs–a hello message and a score.

def score_message(score, name="student"):
    """This is a doc-string, a string describing a function.
        score (float): Raw score
        name (str): Name of student
        str: A hello message.
        float: A normalised score.
    norm_score = (score - 50) / 10
    return f"Hello {name}", norm_score

# Without indentation, this code is not part of function
name = "Ada"
score = 98
# No name entered
# Name entered
print(score_message(score, name=name))
('Hello student', 4.8)
('Hello Ada', 4.8)


What is the return type of a function with multiple return values separated by commas following the return statement?

In that last example, you’ll notice that we added some text to the function. This is a doc-string, or documentation string. It’s there to help users (and, most likely, future you) to understand what the function does. Let’s see how this works in action by calling help() on the score_message function:

Help on function score_message in module __main__:

score_message(score, name='student')
    This is a doc-string, a string describing a function.
        score (float): Raw score
        name (str): Name of student
        str: A hello message.
        float: A normalised score.


Write a function that returns a high five unicode character if the input is equal to “coding for economists” and a sad face, “:-/” otherwise.

Add a second argument that takes a default argument of an empty string but, if used, is added (concatenated) to the return message. Use it to create the return output, “:-/ here is my message.”

Write a doc-string for your function and call help on it.

To learn more about args and kwargs, check out these short video tutorials.


Scope refers to what parts of your code can see what other parts. There are three different scopes to bear in mind: local, global, and non-local.


If you define a variable inside a function, the rest of your code won’t be able to ‘see’ it or use it. For example, here’s a function that creates a variable and then an example of calling that variable:

def var_func():
    str_variable = 'Hello World!'


This would raise an error, because as far as your general code is concerned str_variable doesn’t exist outside of the function. This is an example of a local variable, one that only exists within a function.

If you want to create variables inside a function and have them persist, you need to explicitly pass them out using, for example return str_variable like this:

def var_func():
    str_variable = "Hello World!"
    return str_variable

returned_var = var_func()
Hello World!


A variable declared outside of a function is known as a global variable because it is accessible everywhere:

y = "I'm a global variable"

def print_y():
    print("y is inside a function:", y)

print("y is outside a function:", y)
y is inside a function: I'm a global variable
y is outside a function: I'm a global variable

This is just a taster of what can be done using base Python with few extra packages. For more, especially if you’ve done other chapters in the book already and want to go a bit deeper, see the Chapter on Coding. Otherwise, head on to the next chapter!