Pre-bootcamp Learner Notes

Once you’ve followed the Pre-course Introduction section, follow through these self-guided exercises. This will ensure that you’ve got a grounding in the fundamentals of the Python language and Git, so that you’re ready for Bootcamp..

1. Your first Python program

This topic sees you write your first Python program.

Reading material

Work through Getting Started with Python in VS Code up until Install and use Packages. It will take you through installing python and VSCode as well as setting up your first python script.

Comments

When you start writing files, you will inevitably come across code comments. Some text starting with # is a “comment”. It doesn’t actually do anything – it just tells the reader something interesting. Try it out with a file like this:

# I am a comment
# print('this will not print')

print('this will print') # comment at end of line

In a well-written codebase there shouldn’t be much need for code comments, as the code should generally speak for itself. We describe that as “self-documenting” code.

Bear in mind that comments don’t just take time to write. The cost is ongoing:

  • They obscure the code / take time to read
  • They need to be kept up to date
  • They lie (it’s always a risk even if you try to keep them accurate)

Examples of bad comments:

# x is cost per item
x = 10.99
# print cost per item
print(x)

The need for a comment is often an indication that the code should be improved instead, until it becomes self-documenting and a code comment would just repeat the code. That said, comments can certainly be useful. There are still plenty of reasons to write one. In short, good comments explain why the code is the way it is, rather than how it works.

For example:

  • Working around surprising behaviour or a bug in another tool
  • Explaining why something is not implemented yet
  • Justifying use of a particular algorithm for performance reasons
  • Citation or a link to a useful reference
  • A slightly special case is documentation for a library or API, where the consumer is unable/unwilling to look at the code. Python has docstrings for this purpose.

Exercise 1.1

If you are using our recommended development environment, Visual Studio Code, then you should follow the instructions below to create your first application.

  • Start by creating a new folder for the program (say C:\Work\Training\PreBootcamp) and then open that folder in VSCode (File > Open Folder…)
  • Create a new file (File > New File…) called hello.py
  • Type the following in the VSCode editor, and save it:
message = "Hello World!"
print(message)
  • Open a terminal console in VSCode (Terminal > New Terminal)
  • At the prompt (which should show the project path C:\Work\Training\PreBootcamp), type the following command and press Enter: python hello.py
  • At this step, your program is executed
  • You should see the text Hello World! in the Terminal window – if so then your program works!
  • You can also run your python code by: right-click anywhere in the editor window and select Run Python File in Terminal (which saves the file automatically)

Once you’ve finished reading, work through the exercises below. Make sure you make a note of anything you don’t fully understand.

Exercise 1.2

Having started your PreBootcamp program, we want to put it on GitHub for all the world to see. Or at least, so your tutor can take a look in due course. In git, each project generally lives in its own ‘repository’. Here’s how to create one:

  • Go to GitHub
  • Click the green “New” button to start creating a new repository
  • Enter a suitable name for your repository – “PreBootcamp” would do! Make sure the “Owner” is set to your name. Enter a description if you like too.
  • It would be handy to “Add .gitignore” – choose “Python” as the template. (.gitignore is a file describing the types of files that shouldn’t be stored in git; for example, temporary files or anything else that shouldn’t be shared with other team members. You pick a Visual Studio-specific one so it’s preconfigured with all the files Visual Studio will create that really shouldn’t be stored in git).
  • Leave everything else at the defaults. In particular you want a “Public” project, unless you want to pay for the privilege.
  • Click “Create repository”.

That gives you a (nearly) empty repository. Now you want to link that up to your program. Open up Git Bash (or your command line utility of choice). Navigate to where your HelloWorld code lives (e.g. cd C:\Work\Training\PreBootcamp, or right-click and use ‘git bash here’). Then run the following commands:

git init
git remote add origin https://git@github.com:YourName/PreBootcamp.git
git fetch
git checkout main

Replace “YourName” in the above with your GitHub username. You can find the correct text to use by clicking the green “Code” in your project on GitHub and then finding the “Clone with HTTPS” URL.

Info

We’ll discuss these git commands later in the Bootcamp, and for now you don’t need to worry about what they do exactly. Broadly what we’re doing is setting up a local repository that’s linked to the one on GitHub so changes you make can be uploaded there.

If you’re using a GitHub account you created in 2020 or earlier, you may need to replace main with master above because that used to be the default branch name. If you’ve just signed up for GitHub now that won’t be an issue.

You should find that there are no errors, and that the .gitignore file that you asked GitHub to create now exists locally. However if you refresh your web browser on your GitHub project you’ll see that hasn’t changed – the HelloWorld code is only on your local machine. You can fix this by running this in your command prompt:

git add .
git status
git commit -m "My first piece of Python code"
git push

Now refresh your GitHub browser window and your source code should be visible!

Info

Again, we’ll discuss what these are doing later – for now just remember that you should run these four commands, replacing the text in quotes with a short summary of what you’ve changed, every time you’ve made a change to your code and want to update your GitHub repository with that change.

When you’re prompted to submit your answers exercises during the course, you can just supply the GitHub link – something like https://github.com/YourName/PreBootcamp. Your trainer can see the code, and provide feedback on it if appropriate. You don’t need to submit anything at this stage, you can move on to the next exercise.

Some notes on git

For the time being, you don’t need to worry too much about what the various commands above actually did. However, here are some details to satisfy your curiosity:

  • git init: Turn the current directory into a git repository on your local machine. A hidden directory .git is created to manage all the git internals – the rest of your files stay unchanged.
  • git remote add origin git@github.com:YourName/PreBootcamp.git: Git is a distributed version control system. Your local machine contains a complete and working git repository, but other people can also have a complete and working copy of the git repository. If one of those “other people” is GitHub, that provides a convenient way of sharing code between multiple people. This line just says that GitHub (specifically, your Calculator repository) should have a remote copy, and we’re naming that copy “origin”. The name “origin” is just a git convention meaning “the main copy” – but actually you could use any name, and Git doesn’t really do anything special to make one copy “more important” than another.
  • git fetch: This downloads all the latest changes from GitHub. In this case, that means downloading the .gitignore file to your machine. But it’s not visible on your local machine yet…
  • git checkout main: This tells Git which version of the code you want to see. The “main” branch is the main copy of the code that’s currently being worked on. You’ll notice “Branch: main” is displayed in GitHub too – you can create multiple branches to track progress on different features under development, and this is useful if several people are working on your code at once.
  • git add .: This tells Git to record the changes made to all the files at . which means the current working directory; you could equally well specify each individual file by name.
  • git status: This doesn’t actually do anything, but displays the current status of your repository – you should see some files listed as being changed and ready for commit.
  • git commit -m "My first piece of Python code": This tells Git to save those changes in its history. That way you can go back to this version of the code later, should you need to. Git provides a history of everything that’s happened. The -m precedes a message which explains the purpose of the change.
  • git push: This sends all the changes stored in your local repository up to GitHub. It’s just like the earlier git fetch, but in the opposite direction.

If you want to see the history of your commits, click on the “commits” label in GitHub, or run git log locally. There’s also a local graphical view by running gitk.

Exercise 1.3

Now you’ve set up your git repository this is where you’ll complete the first few exercises of this course.

Make folders named after the following chapters:

  • 2-variables
  • 3-data-types
  • 4-functions
  • 5-control-flow

Inside each folder, add a Python file named exercise_{chapter_number}_1.py. For example, in the 2-variables folder, add a file named exercise_2_1.py.

After creating each file within each folder, update your Git repository using the following commands:

git add .
git status
git commit -m "Added Exercise Folders"
git push

For the next exercises, every time you complete a new exercise, add a new Python file to the appropriate chapter folder. For instance, add files like exercise_2_2.py, exercise_2_3.py, etc., to the 2-variables folder. Remember to commit your answers often to track your progress in the repository.

By following these steps, you can organize and manage your exercise files using Git for the next chapters of the course.

2. Variables

Chapter objectives

In this chapter you will:

  • Learn what variables are, and how they can be used,
  • Practise defining and overwriting variables,
  • Practise using variables in expressions.

What are variables?

Variables provide storage for values, allowing them to be retrieved and used later. The assignment operator (=) is used to define a variable and give it a value:

number_of_apples = 10
print(number_of_apples)
# output: 10

The newly defined variable’s name is number_of_apples, and its value is 10.

Variable Names

  • Variable names can only contain letters, numbers, and underscores.
  • A variable name must not start with a number.
  • The Python convention for variable names is to use snake_case, as shown above. Other languages may use other conventions such as numberOfApples.

Once a variable has been defined, its value can be overwritten by using the assignment operator (=) again:

number_of_pears = 2
number_of_pears = 3
print(number_of_pears)
# output: 3

Variables can also be used in expressions:

number_of_apples = 4
number_of_pears = 5
total_fruit = number_of_apples + number_of_pears
print(total_fruit)
# output: 9

Another way of thinking about variables

Variables can be compared to storage boxes:

VariablesStorage boxes
Defining a variableCreating a box and putting an item it
Overwriting the value in a variableReplacing the item in the box
Accessing a variableChecking what item is in the box

In this analogy, the variable is a storage box, and the variable’s value is an item in the box.

Why use variables?

Variables are sometimes necessary. For example, a program might ask a user to input a value, and then perform various calculations based on that value. If the program didn’t store the value in a variable, it would have to repeatedly ask the user to enter the value for each calculation. However, if the program stored the value in a variable, it would only have to ask for the value once, and then store it for reuse.

Variables are also commonly used to simplify expressions and make code easier to understand.

Can you guess what this calculation is for?

print(200_000 * (1.05 ** 5) - 200_000)
# output: 55256.31...

Underscores (_) can be used as thousands separators in numbers.

It’s actually calculating how much interest would be added to a loan over a period of 5 years if no payments were made.

Imagine this was a line of code in a codebase. Any developers who stumble across this line probably wouldn’t know exactly what the calculation is for. They might have to spend some time trying to figure it out from context, or even track down the person who wrote it.

Ideally, we would find a way to write this calculation so that its purpose is immediately clear to everyone. This approach makes it much easier to revisit old code that you previously wrote, and it’s absolutely vital when collaborating with other developers.

initial_loan_value = 200_000
interest_rate = 1.05
number_of_years = 5

current_loan_value = initial_loan_value * (interest_rate ** number_of_years)
total_interest = current_loan_value - initial_loan_value

print(total_interest)
# output: 55256.31...

Unfortunately, we’re now using a few more lines of code than before. However, more importantly, anyone who comes across this code will have a much easier time understanding it and working with it. Concise code is desirable, but it shouldn’t come at the expense of readability!

Other resources

If you’d like to read some alternate explanations, or see some more examples, then you might find these resources helpful:

Practice exercises

We’ve run through the general concepts, and now we’ll get some hands-on experience.

It can be tempting to jump right into running each exercise in the VSCode, but it’s best to try and predict the answers first. That way, you’ll have a clearer idea about which concepts you find more or less intuitive.

Exercise 2.1

Use the VSCode to run these commands:

my_first_number = 5
my_second_number = 7
my_third_number = 11
my_total = my_first_number + my_second_number + my_third_number
print(my_total)
#<answer>

Exercise 2.2

Use the VSCode to run these commands:

my_number = 3
my_number = 4
print(my_number)
#<answer>

Exercise 2.3

Use the VSCode to run these commands:

my_first_number = 5 * 6
my_second_number = 3 ** 2
my_third_number = my_first_number - my_second_number
print(my_third_number)
#<answer>

Troubleshooting exercises

There’s a few issues that people can run into when using variables in Python. We’ve listed some of the most common ones here.

For each troubleshooting exercise, try and figure out what went wrong and how it can be fixed.

You can check your answers for each exercise at the end of this chapter.

Exercise 2.4

Why is an error being printed?

my_first_number = 1
my_third_number = my_first_number + my_second_number
# output:
NameError: name 'my_second_number' is not defined

Exercise 2.5

Why is an error being printed?

my_first_number = 2
my_second_number = 3
my_first_number + my_sedond_number
# output:
NameError: name 'my_sedond_number' is not defined

Exercise 2.6

Why is an error being printed?

my_first_number = 1
my_second_number =
# output:
SyntaxError: invalid syntax

Exercise 2.7

Why is an error being printed?

my first number = 1
# output:
SyntaxError: invalid syntax

Exercise 2.8

Why is an error being printed?

my-first-number = 1
# output:
SyntaxError: cannot assign to operator

Answers

Exercise 2.4

Python requires variables to be defined before they can be used. In this case, a variable called my_second_number is used without first being defined.

Exercise 2.5

The variable called my_second_number is misspelled as my_sedond_number.

Exercise 2.6

The assignment operator requires both a variable name and a value. In this case, the value is omitted.

Exercise 2.7

Variable names can only contain letters, numbers, and underscores. In this case, a space is used in the variable’s name.

Exercise 2.8

Variable names can only contain letters, numbers, and underscores. In this case, hyphens are used in the variable’s name.

Summary

We’ve reached the end of chapter 2, and at this point you should know:

  • What variables are, and how they can be used to store values,
  • How to define and overwrite variables,
  • How to use variables in expressions.

3. Data Types

Chapter objectives

We’ve learned how to perform calculations, but computers are more than just calculators. You will need to handle some things other than numbers.

In this chapter you will:

  • Learn what data types are
  • Practise using some common types that are built into Python
  • Numbers
  • Booleans
  • Strings
  • Lists
  • Dictionaries

Follow along on VSCode to check you can use the data types as shown in the examples.

What is a data type?

There are fundamentally different kinds of data that look and behave differently. Are you calculating a number or building a list of users? You should try to stay aware of the “type” of every expression and variable you write.

If you try to treat a piece of text like a number, your command might fail (aka “raise an exception”), or it might just behave surprisingly.

For example, let’s return to the first operator we looked at: +. The meaning of x + y is very different depending on what exactly x and y are.

If they are numbers, it adds them as you expect:

print(1 + 2)
# output: 3

But if they are text values (known as “strings”), then it joins them together (a.k.a. concatenates them):

print('Hello, ' + 'World!')
# output: 'Hello, World!'
print('1' + '2')
# output: '12'

If x and y are different types or don’t support the + operator, then Python might fall over:

print('Foobar' + 1)
# output:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

The error message on the last line is trying to explain the problem. It says that you can only add strings to other strings (abbreviated to str). You cannot add an integer (abbreviated to int) to a string.

Now let’s look at some data types in more detail. For a more thorough list of Python’s built-in types, you could consult the official docs, but we’ll go over the essentials here.

Numbers (int, float)

We have so far only used numeric values. It is worth being aware that there are actually different types of numbers – whole numbers (aka integers) or non-integers (aka floating point numbers). The abbreviations for these are “int” and “float”.

This is often not a big deal as Python will happily run expressions combining integers and floats, as you may have done already, but they are in fact different types.

print(1 + 2)
# output: 3

print(1 + 2.0)
# output: 3.0

print(1.5 + 1.6)
# output: 3.1

print(3 == 3.0)
# output: True

Something to be aware of is that floating point calculations inevitably involve rounding. There is a separate type, Decimal, that solves the problem for many scenarios, but that goes beyond the scope of this course.

True / False (bool)

There are two boolean values, True and False, which are fairly self-descriptive. There are various operators that can produce booleans by comparing values. For example:

  • 1 < 2 means “1 is less than 2”, so this returns True. Similarly, > means “greater than”.
  • 1 <= 2 means “1 is less than or equal to 2”. Similarly, >= does what you expect.
  • 1 == 2 means “1 is equal to 2”, so this returns False. Notice there must be two equals signs, as a single equals sign is used for assigning values, e.g. when creating a variable.
  • 1 != 2 means “1 is not equal to 2”, so this returns True.

Try writing some comparisons and check that they return what you expect.

There are also Boolean operators for combining them:

  • a and b is True if both a and b are True
  • a or b is True if either of a or b (or both) are True
  • not a is True if a is False
print(True and False)
# output: False

print(True or False)
# output: True

print(not False)
# output: True

In Python, you can actually use other data types in Boolean expressions. Most values will be considered True but some special values are False, such as 0.

Exercise 3.1

Copy these two lines into the VSCode. Write a single expression that uses these variables to tell you whether you’re going to have a bad time on your walk.

it_is_raining = True
you_forgot_your_umbrella = True

Can you do the same thing using these variables?

it_is_raining = True
you_have_your_umbrella = False
Click here for the answer
print(it_is_raining and you_forgot_your_umbrella)
# output: True
print(it_is_raining and not you_have_your_umbrella)
# output: True

Text (strings)

A text value is known as a string (or abbreviated to str). To create a string, just write some text wrapped inside quotation marks, like "Hello". This is called a string literal (a “literal” just means “what you’d need to type in code to get that value” – so "Apple" is the literal for the string “Apple”, 4 is the literal for the integer 4, and you’ll see later in this document list literals [1, 2, 3] and dictionary literals { name: 'Joseph' }). You can use single or double quotes to make your string – the only difference is the ease of including quotation marks inside the text itself. If you want to include the same type of quotation mark as part of the text value, you need to “escape” it with a backslash.

print("example text" == 'example text')
# output: True

print("I'm a string" == 'I\'m a string')
# output: True

Building a string

To build a string using some other values, you have three options:

  1. You can try putting it together yourself with the + operator. “String concatenation” means joining two or more values together, one after the other. Note that this will not convert other data types to strings automatically.
print('I have ' + '2 apples')
# output: 'I have 2 apples'

number_of_apples = 2
print('I have ' + str(number_of_apples) + ' apples')
# output: 'I have 2 apples'

There are built-in functions to convert between data types. str(2) converts the number 2 into a string, ‘2’. Similarly, int('2') converts a string into an integer. We will look at functions in more detail next chapter, but for now, try using these – you write the function name, like str, followed by parentheses around the value you want to convert.

  1. A formatted string literal or “f-string” is written with an f before the first quote. It lets you insert values into the middle of a string. This pattern is known as “string interpolation” rather than concatenation and is often the clearest way to write it. You can also specify details, like how many decimal places to display.
number_of_apples = 2
print(f'I have {number_of_apples} apples')
# output: 'I have 2 apples'

kilos_of_apples = 0.4567
print(f'I have {kilos_of_apples:.3f}kg of apples')
# output: 'I have 0.457kg of apples'
  1. There is another way of performing interpolation – the format method of strings. There can be situations where it looks neater, but this is often just the older and slightly messier way of doing the same thing. One useful application of format is storing a template as a variable to fill in later.
number_of_apples = 2
print('I have {} apples'.format(number_of_apples))
# output: 'I have 2 apples'

template = 'I have {} apples'
print(template.format(number_of_apples))
# output: 'I have 2 apples'

Exercise 3.2

If you’ve heard of Mad Libs, this exercise should be familiar. We’ll pick some words and then insert them into a template sentence.

Set these four variables with whatever values you like:

plural_noun = 'dogs'
verb = 'jump'
adjective_one = 'quick'
adjective_two = 'lazy'
  1. Below that, write an expression that produces text like the following, where the four emphasised words are inserted using the correct variables.
Computers are making us *quick*, more than even the invention of *dogs*. That's because any *lazy* computer can *jump* far better than a person.
  1. Now let’s generate text that puts the input word at the start of a sentence. You can use the capitalize method of a string to capitalise the first letter. E.g. 'foobar'.capitalize() returns 'Foobar'.

Use that to build this sentence:

*Quick* *dogs* *jump* every day. *Lazy* ones never do. 
Click here for the answer
  1. Note there is a ' in the text, so let’s use double quotes to wrap our string. The easiest form to read is an f-string:
print(f"Computers are making us {adjective_one}, more than even the invention of {plural_noun}. That's because any {adjective_two} computer can {verb} far better than a person.")

Or store the template in a variable to easily recreate it multiple times, with different values.

template = "Computers are making us {}, more than even the invention of {}. That's because any {} computer can {} far better than a person."
print(template.format(adjective_one, plural_noun, adjective_two, verb))
  1. You can call the capitalize method inside the f-string:
print(f'{adjective_one.capitalize()} {plural_noun} {verb} every day. {adjective_two.capitalize()} ones never do.')

Character access

You can access a specific character in a string with square brackets and the “index”, which means the position of the character but counting from 0. Or you can consider it an offset. Either way, 0 means the first character, 1 means the second, and so on. It looks like this:

print('abc'[0])
# output: 'a'

print('abc'[1])
# output: 'b'

Exercise 3.3

Can you to print out the fourth digit of this number? You will need to convert it to a string first, and then access a character by index.

my_number = 123456
Click here for the answer
my_number = 123456
my_string = str(my_number)
print(my_string[3])
# output: 4

The two actions can also be written on a single line:

print(str(my_number)[3])
# output: 4

Lists

A list is a way of grouping multiple items together in a single object (in a well-defined order). Each item in the list will usually be the same type, but they can be anything, e.g. mixing numbers and strings. Mixing types is not typical and requires a lot of care.

You create a list by opening a square bracket, writing values with commas between them, and then close the square brackets:

shopping_list = ['milk', 'bread', 'rice']
some_prime_numbers = [2, 3, 5, 7, 11]

You can access an individual item in the list by writing the index of the item you want in square brackets. The index is the position of the item, but starting at 0 for the first item. This is just like accessing a character in a string.

print(shopping_list[0])
# output: 'milk'

print(shopping_list[1])
# output: 'bread'

Rather than grabbing one by position, you will often want to go through the list and act on each item in turn. We will cover that in chapter 5.

We will discuss functions next chapter but here are three functions worth trying out:

  • len gives you the length of a list
  • append is a function on the list itself that will add an item onto the end of the list

Here is how to use them:

shopping_list = ['milk', 'bread', 'rice']
print(len(my_shopping_list))
# output: 3

my_shopping_list.append('eggs')
print(my_shopping_list)
# output: ['milk', 'bread', 'rice', 'eggs']

Try these out in VSCode

Exercise 3.4

Given a list of prime numbers, write an expression to get the tenth one.

some_prime_numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
Click here for the answer
print(some_prime_numbers[9])
# output: 29

Dictionaries

This is another way of storing a collection of things. But instead of storing them in a list, you associate each one with a label for ease of access. They are called dictionaries because they can be used similarly to an actual dictionary – looking up the definitions of words. In this usage, the word you are looking up is the key and the definition is the value. You don’t want to look up a definition based on its position in the dictionary, you want to look up a particular word.

Here is an example dictionary:

my_dictionary = { 'aardvark': 'insectivore with a long snout', 'zebra': 'stripy horse' }

You write a dictionary with curly brackets instead of square. Inside it you have a comma-separated collection of key/value pairs. Each pair is written as key: value. The key will often be a string, in which case remember to wrap it in quotes. You can have as many key/value pairs as you want.

To retrieve a value, you use square brackets and the corresponding key:

print(my_dictionary['zebra'])
# putput: 'stripy horse'

This is the syntax to add new key/value pairs, or update an existing one:

my_dictionary['bee'] = 'buzzing thing'
print(my_dictionary)
# output: {'aardvark': 'insectivore with a long snout', 'zebra': 'stripy horse', 'bee': 'buzzing thing'}

Unlike a real dictionary we don’t worry about storing the entries alphabetically, as we will access them directly.

Try creating, reading and updating some dictionaries via the Python terminal. The key can be a number, string or boolean, and must be unique – only the latest value you set for that key will be kept. The value can be anything at all.

Exercise 3.5

I’ve finally gone shopping and have added two cartons of eggs to my shopping basket. Here is a Python dictionary to represent them:

eggs = { 'name': 'Free Range Large Eggs', 'individual_price': 1.89, 'number': 2 }

Can you print the price per carton of eggs? Then can you calculate the total price?

Click here for the answer
eggs = { 'name': 'Free Range Large Eggs', 'individual_price': 1.89, 'number': 2 }
print(eggs['individual_price'])
# output: 1.89
print(eggs['individual_price'] * eggs['number'])
# output: 3.78

Exercise 3.6

I would like an easy way to check who is currently staying in each room of my hotel. Define a dictionary, which maps room numbers to lists of people (strings) occupying each room.

Put two guests in room 101, no guests in room 102, and one guest in room 201.

Click here for the answer
rooms = { 101: ['Joe Bloggs', 'Jane Bloggs'], 102: [], 201: ['Smith'] }

Exercise 3.7

Using your dictionary from the previous question, can you write expressions to find the answers to the following questions?

  • Who is staying in room 101?
  • How many guests are staying in room 201?
  • Is room 102 vacant?
Click here for the answer
  • Get the value for the desired key: rooms[101]. This returns a list of strings.
  • Get the length of a list with the len function: len(rooms[201]). This returns an integer
  • Check if the length of the list is zero. This returns a boolean:
    • len(rooms[101]) == 0 returns False
    • len(rooms[102]) == 0 returns True

There’s an alternative answer for the last part. You could take advantage of the fact that an empty list is “falsy”, meaning it is treated like False when used as a boolean. A list of any other size is “truthy”. To answer the question “is room X vacant”, reverse that boolean with not.

  • not [] will return True (i.e. the room is empty)
  • So you can write not rooms[102]

Exercise 3.8

It’s very common for dictionaries to contain smaller dictionaries.

Here is a dictionary with information about a user. The user has a name, age and address. The address is itself a dictionary.

user = { 'name': 'Jamie', 'age': 41, 'address': { 'postcode': 'W2 3AN', 'first_line': '23 Leinster Gardens' } }
  • Write an expression to get the user’s address (the nested dictionary).
  • Write an expression to get just the user’s postcode (just a single string).
Click here for the answer
print(user['address'])
# output: { 'postcode': 'W2 3AN', 'first_line': '23 Leinster Gardens' }
print(user['address']['postcode'])
# output: 'W2 3AN'

When you have no data at all

There is a special value in Python, None, which has its own type, NoneType. It can be used to represent the absence of a value, such as when optional fields are missed out. It is also the result of a function that doesn’t return anything else. Nothing will print out when an expression evaluates to None, but it’s still there.

You may also end up encountering it because of a mistake. For example, do you remember the .append() method of lists that modifies an existing list? It doesn’t return anything, so look at the behaviour of the following code:

x = [1, 3, 2]
y = x.append(4)
print(y == None)
# output: True

y
print(str(y))
# output: 'None'

print( y[0])
# output:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable

Here is an example of using None deliberately:

Dictionaries have a get method, that you can use instead of the my_dictionary['zebra'] syntax. The difference is what happens when the dictionary doesn’t contain that key. The square brackets will throw an error but the get method will calmly return None. Try it out:

my_dictionary = {'zebra': 'stripy horse'}
print(my_dictionary.get('zebra'))
# output: 'stripy horse'

my_dictionary.get('grok')
print(my_dictionary.get('grok') == None)
# output: True

Or you could specify a different fallback value for get if you want, by putting it in the parentheses.

print(my_dictionary.get('grok', 'no definition available'))
# output: 'no definition available'

Troubleshooting exercises

There are a few issues that people can run into when using different data types in Python. We’ve listed some common ones here.

For each troubleshooting exercise, try and figure out what went wrong and how it can be fixed.

Exercise 3.9

The final command here is throwing an error:

budget = '12.50'
expenditure = 4.25 + 5.99
expenditure < budget

Can you fix this? It should return True.

Exercise 3.10

What is wrong with the following block of code? Can you fix the mistake?

my_string = 'Hello, world'
my_dictionary = { 'greeting': 'my_string', 'farewell': 'Goodbye, world' }
Click here for a hint

What does my_dictionary['greeting'] return?

Click here for the answer
my_string = 'Hello, world'
my_dictionary = { 'greeting': my_string, 'farewell': 'Goodbye, world' }

Exercise 3.11

Why does this produce an error instead of the string ‘Alice’? Can you fix the mistake?

user_by_id = { '159': 'Alice', '19B': 'Bob' }
print(user_by_id[159])
# output:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 159
Click here for the answer

The error is saying that there is no key 159 because the key is a string '159', not a number. When you try to fetch a value, you need to provide the correct key and the number 159 is not equal to the string '159'.

print(user_by_id['159'])
# output; 'Alice'

Summary

We’ve reached the end of chapter 3, and at this point you should know:

  • What data types are and why you need to be aware of the type of everything you use.
  • How to use some basic types:
  • Numbers
  • Booleans
  • Strings
  • Lists
  • Dictionaries

Other built-in types exist, and there is also a lot of functionality in these types that we haven’t covered yet, but you now have the essentials. We will look at defining your own types (aka classes) in a later chapter.

4. Functions

Chapter objectives

At some point, as you try to achieve something more complex, you will want to divide your script into reusable chunks.

In this chapter you will:

  • Write and execute a Python file
  • Learn what functions, arguments and return values are
  • Practise writing and using functions
  • Look at a few useful functions that come built into Python.

What is a function?

We’ve learnt about some of the basic components of Python code, but to go from a simple program to more complex behaviour, we need to start breaking up that program into meaningful blocks. Functions will let us structure our code in a way that is easier to understand, and also easier to update.

A function is a block of code, ie a set of instructions. You give it a name when you define it, and then you “call” the function later to actually execute that code. Some functions will want you to pass data in, and some functions can return a result, but the simplest function definition is just sticking a label on a few lines of code.

You could also think of the function as a machine that you can operate as many times as you like. Its name should describe its purpose. It may take some input and/or produce some output. For example, a blender takes some ingredients as input, blends them together and you can pour out the output. Some functions could have neither, like winding up a toy car and watching it go.

To make our code as “clean” as possible, we try to write functions that are well-named and short. Each one should serve a single purpose, not try to be a Swiss army knife.

Basic syntax

To define a function in Python, start with the def keyword, followed by a name, a pair of parentheses and a colon. If you use the wrong type of brackets or forget the colon, it will not work. After that, write one or more indented lines of code. Python is sensitive to indentation and will use it to see where the function ends.

Here’s a simple example:

def hello_world():
    print('Hello, World!')

Python’s naming convention is snake_case for function names, just like variable names. The rules for a valid function name are the same as variable names, e.g. no spaces allowed.

Any indented lines of code below the def line are part of the function. When you stop indenting code, the function is complete and you are outside it again.

Copy the above example into a Python file and try running it. You’ll see that it didn’t actually say hello. To call a function, write its name followed by a pair of brackets. Try running a file that looks like this, and you should see the message appear in your terminal:

def hello_world():
    print('Hello, World!')

hello_world()

Try defining and calling some functions.

Methods

A function can also belong to an object; this is called a method. For example, strings have an upper method to return an uppercase copy of the string. The original is left unchanged. To call a method, you put a dot and the function name after the object. Try it out:

whisper = 'hello, world'
shout = whisper.upper()
print(whisper)
# output: 'hello, world'

print(shout)
# output: 'HELLO, WORLD'

Some methods might modify the object instead of just returning a value. We will look at defining our own methods when we create classes in a later chapter.

Exercise 4.1

Lists have a sort method, which reorders the list from lowest value to highest. It modifies the list, and does not return anything. Create a list and assign it to a variable by adding the line of code below.

my_list = [1, 4, 5, 2, 3]

Now try to sort that list and check it looks how you expect.

Click here for the answer
my_list = [1, 4, 5, 2, 3]
my_list.sort()
print(my_list)
# output: [1, 2, 3, 4, 5]

Input

Functions often want to perform an action that depends on some input – your calculator needs some numbers, or your smoothie maker needs some fruit. To do this, name some parameters between the parentheses when defining the function. Inside the function, these will be like any other variable. When you call the function, you provide the actual values for these, aka arguments.

The terms “argument” and “parameter” are often used interchangeably. Technically, a parameter refers to what is in the function definition and argument is the value that is passed in, but no one will really notice if you use them the other way around.

For example, create a file containing this code:

def print_total_interest(initial_loan, interest_rate, number_of_years):
    repayment_amount = initial_loan * (interest_rate ** number_of_years)
    total_interest = repayment_amount - initial_loan
    print(f'£{total_interest:.2f}')

print_total_interest(1000, 1.05, 10)

We define a function that takes three arguments. We provide three numbers in the correct order when we call the function. You should see £628.89 printed to your terminal when you run the file.

Note that the variables created inside the function only exist inside the function – you will get an error if you try to access total_interest outside of the function.

When you call a function, you can explicitly name arguments as you pass them in, instead of relying on the order. But you are now reliant on the names. E.g.

print_total_interest(number_of_years=10, initial_loan=1000, interest_rate=1.05)

These are called keyword arguments and are most common when there are optional parameters.

Output

Maybe you don’t just want to print things. Maybe you want to get a number back for the total interest so you can do something else with it. To get it out of the function, you need a return statement. This determines the output of your function, and also exits the function.

For example:

def get_total_interest(initial_loan, interest_rate, number_of_years):
    repayment_amount = initial_loan * (interest_rate ** number_of_years)
    return repayment_amount - initial_loan

interest = get_total_interest(1000, 1.05, 10)
print(interest)

You could have multiple return statements when you have some logic to decide which line gets executed. We will see some examples of this next chapter.

Practice exercises

We’ve run through the general concepts, and now we’ll get some more hands-on experience.

Write your solution in a Python file and execute it (either from the command line or VSCode) to check it works.

You can check your answers for each exercise as you go

Exercise 4.2

Define a function called greet_user, that has one argument, name. It should print out a greeting such as Hello, Smith, but using the name that was given as a parameter.

Click here for the answer
def greet_user(name):
    print('Hello, ' + name)

Exercise 4.3

Now add these lines to the bottom of your file from the previous question. And after that, call your greet_user function to greet them.

name_one = 'Jane'
name_two = 'Joe'
Click here for the answer
greet_user(name_one)
greet_user(name_two)

Exercise 4.4

Write a function that has one parameter – a list of words – and returns the word that comes first alphabetically.

Call your function on a list of words to show it works.

Click here for a hint

First, use the .sort() method on the list. Next, return the word at index 0.

Click here for the answer
def get_first_word_alphabetically(words):
    words.sort()
    return words[0]

my_first_word = get_first_word_alphabetically(['Zebra', 'Aardvark'])
print(my_first_word)

Troubleshooting exercises

For each troubleshooting exercise, try and figure out what went wrong and how it can be fixed.

Exercise 4.5

Can you fix this script so that the final line prints “Success!”?

def do_the_thing():
    # pretend this function does something interesting
    return 'Success!'

result = do_the_thing
print(result)
Click here for the answer

You need to include brackets to call the function:

result = do_the_thing()

Exercise 4.6

If you try to put this function definition in a file, you get an error. Running the function should print the number 2. Can you fix it? Note, there are multiple issues.

def do_the_thing[]
two = 1 + 1
print(two)
Click here for the answer
  • You need parentheses, not square brackets
  • You need a colon
  • The contents need to be indented
def do_the_thing():
    two = 1 + 1
    print(two)

Exercise 4.7

What do you think this code does?

def make_a_sandwich(filling):
    sandwich = ['bread', filling, 'bread']
    return sandwich
    print(sandwich)

my_sandwich = make_a_sandwich('cheese')

If you try running this, you will see nothing is getting printed out. Can you fix it?

Click here for the answer

The return statement will exit the function – later lines will not execute. So let’s move it after the print statement.

def make_a_sandwich(filling):
    sandwich = ['bread', filling, 'bread']
    print(sandwich)
    return sandwich

my_sandwich = make_a_sandwich('cheese')

Summary

We’ve reached the end of chapter 4, and at this point you should know:

  • What functions are and how they help you structure/reuse code.
  • How to define functions, including specifying parameters
  • How to invoke functions, including passing in arguments

See below for some extra points regarding functions in Python:

Some extras

Empty functions

Note that a function cannot be empty. If the function is totally empty, the file will fail to run with an “IndentationError”. But maybe you want to come back and fill it in later? In that case, there’s a keyword pass that does nothing and just acts as a placeholder.

def do_nothing():
    pass

do_nothing()

Functions as objects

In Python, functions are actually objects with a data type of “function”. If you don’t put brackets on the end to call the function, you can instead pass it around like any other variable. Try running the following code:

def my_function():
    print('My function has run')

def call_it_twice(function):
    function()
    function()

call_it_twice(my_function)

Decorators

You will eventually come across code that looks like the following:

@example_decorator
def do_something():
    pass

The first line is applying a decorator to the function below. The decorator transforms or uses the function in some way. The above code won’t actually work unless you define “example_decorator” – it’s just illustrating how it looks to use a decorator.

5. Control Flow

Chapter objectives

When you need to perform some logic rather than just carrying out the same calculation every time, you need control flow statements. The phrase “control flow” means how the program’s “control” – the command currently being executed is the one in control – flows from line to line. It is sometimes called “flow of control” instead.

In this chapter you will learn how to use Python’s control flow blocks:

  • Conditional execution: if / elif / else
  • Loops: while / for

Like last chapter, try writing your code in a file rather than in the terminal, as we will be writing multi-line blocks.

Conditionals: if

There will inevitably be lines or blocks of code that you only want to execute in certain scenarios. The simplest case is to run a line of code if some value is True. You can try putting this code in a file and running it:

it_is_raining = True

if it_is_raining:
    print('bring an umbrella')

Set it_is_raining to False instead, and the print statement will not run.

Any expression can be used as the condition, not just a single variable. You will often use boolean operators in the expression, for instance:

if you_are_happy and you_know_it:
    clap_your_hands()

Python will actually let you put any data type in the if statement. Most objects will be treated as True, but there are a few things which are treated as False:

  • 0
  • An empty string: ''
  • An empty collection, like [] or {}
  • None

A little care is needed but it means you can write something like if my_list: as shorthand for if len(my_list) > 0:

A point on indentation

The if statement introduces an indented code block similarly to function definitions. If you are already writing indented code and then write an if statement, you will need to indent another level. There’s no real limit to how far you can nest code blocks, but it quickly becomes clumsy.

def do_the_thing():
    if True:
        print('Inside an if-block, inside a function')
        if True:
            print('We need to go deeper')
    print('Inside the function, but outside the ifs')

print('Outside the function')
do_the_thing()

else

Maybe you have two blocks of code, and you want to execute one or the other depending on some condition. That’s where else comes in.

def say_hello(name):
    if (name == 'world'):
        print('Enough of that now')
    else:
        print(f'Hello, {name}!')

say_hello('world')
say_hello('friend')

You can also write an expression of the form result_one if my_bool else result_two. This is known as the ternary or conditional operator. This will check the value of my_bool and then return result_one or result_two, if my_bool was True / False respectively. You can write any expression in place of result_one/my_bool/result_two For example: greeting = f'Hello, {name}!'

Conditionals: elif

There is also elif (an abbreviation of “else if”). Use this when you have a second condition to check if the first one was False, to decide whether to carrying out a second action instead. It needs to follow immediately after an if block (or another elif block), and like an if statement, you provide an expression and then a colon. You can follow the elif with an else statement, but you don’t have to.

Here is an example where we will potentially apply one of two discounts, but not both.

if customer.birthday == today:
    apply_birthday_discount()
elif number_of_items > 5:
    apply_bulk_order_discount()

You can also use it to check a third or fourth condition… in fact, there’s no limit to how many elif statements you can join together. Note that the code will only execute for the first True condition, or the else will execute if none of the conditions were met. So you want to put the more specific checks first. Try running this code and check it prints what you expect.

number_of_apples = 1
if number_of_apples > 10:
    print('I have a lot of apples')
elif number_of_apples > 5:
    print('I have some apples')
elif number_of_apples == 1:
    print('I have an apple')
else:
    print('I have no apples')

Exercise 5.1

Modify this code so that it does apply both discounts when both conditions are True.

birthday_is_today = True
number_of_items = 10
price = 10.00

if birthday_is_today:
    price = price * 0.85
elif number_of_items > 5:
    price = price * 0.9

print(price)
Click here for the answer

Change the elif to an if and both blocks can execute:

birthday_is_today = True
number_of_items = 100

if birthday_is_today:
    print('apply birthday discount')
if number_of_items > 5:
    print('apply bulk discount')

Exercise 5.2

Write a function that takes a string as input and returns the middle character of the string. If there is no middle character, return None instead.

E.g. your_function('abcd') should return None and your_function('abcde') should return 'c'

Click here for hints
  • len(my_string) will give you the length of a string
  • my_int % 2 will give 1 or 0 for odd and even numbers respectively
  • x // 2 will give you the integer result of x divided by 2.
Click here for the answer
def get_middle_character(input):
    length = len(input)

    # Return None for strings with an even length
    if length % 2 == 0:
        return None

    # Otherwise return the middle character
    # This could be inside an "else:" block but there is no need to.
    return input[length // 2]

# For example this will print 'c'
print(get_middle_character('abcde'))

Loops: for

What if you want to repeat a block of code a hundred times without having to write my_function() a hundred times? And maybe you want to keep running it forever?

Or what if you have a sequence, like a list of objects and you want to do something with each of them in turn?

This is where loops come in. There are two different loops: for and while.

The for loop lets you run a block of code repeatedly. It’s called a loop because the last line connects back to the start. The syntax to start the “for loop” is for loop_variable in an_iterable:.

  • An iterable is actually quite general. It’s any object that you can ask for the “next item”. A straightforward example is a list.
  • You can pick any variable name in the place of loop_variable.
  • This for line is followed an indented block of code, just like ifs or functions.
  • On each trip around the loop, the “loop variable” will automatically get updated to the next item in the iterable.

Try running the following example. The print statement will run five times because the list contains five items, and the number variable will have a different value each time:

for number in [1, 2, 3, 4, 5]:
    print(number)

A list is a “sequence” – simply an ordered collection of values. We have already seen another example of sequences: strings. Similarly to lists, you can loop over each character in a string:

for character in 'foobar':
    print(character)

Dictionaries are an example of an iterable that is not a sequence. Its items are not in an indexed order, but you can write for i in my_dictionary:. If you do, then i will be equal to each key of the dictionary in turn. If you want the key and value, then you can use for key, value in my_dictionary.items(): – this will iterate over each key/value pair in turn.

Exercise 5.3

Add together all of the numbers in a list (without using the built-in sum function)

For example, given this line of code, can you write code that will print 100?

number_list = [5, 15, 30, 50]
Click here for a hint

Before you start the loop, create a variable to hold the running total. Add to the running total inside the loop.

Click here for the answer
number_list = [5, 15, 30, 50]
result = 0
for number in number_list:
    result += number # the same as: result = result + number
print(result)

Exercise 5.4

Write a function, find_strings_containing_a, which takes a list of strings and returns just the ones containing the letter ‘a’. So after you define the function, the following code should print ['some cats', 'a dog'] to the terminal.

full_list = ['the mouse', 'some cats', 'a dog', 'people']
result = find_strings_containing_a(full_list)
print(result)

Use the in operator to check if one string is contained within another string.

  • 'foo' in 'foobar' is True
  • 'x' in 'foobar' is False.

You can use the append method of lists to add an item to it.

Click here for the answer
def find_strings_containing_a(strings):
    result = []
    for string in strings:
        if 'a' in string:
            result.append(string)
    return result

This is a good example of where you might want a list comprehension instead of a for-loop:

def find_strings_containing_a(strings):
    return [ string for string in strings if 'a' in string]

range

A useful function for generating a sequence of numbers to iterate over. The result is its own data type, a range, but you can loop over it just like looping over a list.

The syntax is range(start, stop, step). All three parameters are integers, but can be positive or negative

  • start is the first number in the range. It is optional and defaults to 0.
  • stop is the when the range stops. It is not inclusive, i.e. the range stops just before this number.
  • step is the size of the step between each number. It is optional and defaults to 1.

This example will print the numbers 0 to 9 inclusive:

for i in range(10):
    print(i)

If you provide two arguments, they are used as start and stop. So this example will print the numbers 11 to 14 inclusive:

for i in range(11, 15):
    print(i)

Here’s an example with all three parameters, and using negative numbers. Can you correctly guess what it will print?

for i in range(10, -10, -2):
    print(i)

Note that if the range would never end, then it is empty instead. E.g. a loop over range(1, 2, -1) will simply do nothing.

Exercise 5.5

Write a function that prints a piece of text 100 times. But please use a for loop and a range, rather than copying and pasting the print statement 100 times.

Click here for the answer
def print_a_hundred_times(text):
    for current in range(100):
        print(text)

Exercise 5.6

Write a function that takes a positive integer n, and returns the sum total of all square numbers from 1 squared to n squared (inclusive).

For example, with n = 3 your function should return 14 (equal to 1 + 4 + 9)

Click here for the answer
def sum_squares_to_n(n):
    result = 0
    for current in range(1, n + 1):
        result += current**2
    return result

Loops: while

There’s another type of loop that checks a condition each loop (potentially forever) rather than going through each item in a collection.

Do you understand what the following script does? What will it print?

x = 1
while x < 100:
    x = x * 2

print(x) 
Click here for the answer

It will keep doubling x until it is over 100 and then print it out. So in the end it prints 128

For a loop that never ends, you can use while True:. If you accidentally end up with your terminal stuck running an infinite loop, then press Ctrl + C to interrupt it.

break and continue

Two keywords to help navigate loops are break and continue. These apply to both while and for loops.

  • break exits the for loop completely. It “breaks out” of the loop.
  • continue ends the current loop early and “continues” to the next one

Read the following code and see if you can predict what will get printed out. Then run it and check your understanding.

for i in range(10):
    print(f'Start loop with i == {i}')
    if i == 3:
        print('Break out')
        break
    if i < 2:
        print(f'Continue')
        continue
    print('End loop')

Exercise 5.7

Can you make this code print just A, B and C, by adding some code before the print statement? Don’t modify the two lines of code already there, just add some more lines of code before the line print(letter)

for letter in ['A', 'B', 'X', 'C', 'D', 'E']:

    # your code here

    print(letter)    
Click here for the answer
for letter in ['A', 'B', 'X', 'C', 'D', 'E']:
    if letter == 'D':
        break
    if letter == 'X':
        continue
    
    print(letter)

Revisiting the Shopping Cart

You can now revisit Exercise 3 – the shopping cart exercise. This time, write a function that takes shopping_cart as a parameter, and prints out the three values as before. But you should now loop over the list of purchases so you can handle any shopping cart.

To make it even better, add a second function parameter, discounts. It should be a list of the current discounts, for example: [{'item': 'apple', 'discount': '0.5'}]. Instead of your script always discounting apples by 50%, instead check each item in the shopping cart for a matching item in the list of discounts.

Summary

We’ve now reached the end of chapter 5. At this point you should know how to use:

  • if / elif / else statements
  • while or for loops
  • The range function
  • The keywords break and continue inside loops

6. Packages, Modules and Imports

Chapter objectives

You will at some point need to use Python code that is not all contained in one file. You will either use code other people wrote, or you will be splitting your codebase into multiple files to make it easier to navigate. Probably both.

Additionally, when using code from other projects, you should make a note of this dependency on the other project, what version you are using and have an easy way of downloading it. We will use Poetry for this.

In this chapter you will learn:

  • What Python modules are
  • The various ways you can import.
  • How to get started with Poetry to manage your dependencies.

What are these words?

The simple picture is:

  • Each Python file is a module
  • A folder is a package. If the folder contains files/subfolders, the package contains modules/subpackages.
  • For your module (file) to access code from another, you must first import it

By importing a module, you ensure the file has run and you get access to anything that it has defined (like variables or functions).

This analogy doesn’t always hold up – packages and modules don’t need to derive from the file system – but it’s correct for most purposes.

For example, look at this file structure:

parent/
    __init__.py
    module_a.py
    first/
        __init__.py
        module_b.py
        module_c.py
    second/
        __init__.py
        module_b.py

Here, we have a parent package containing a module (module_a), and two subpackages, first and second. The subpackages contain their own modules. There’s nothing stopping them reusing filenames. The convention is to use short, lowercase names. Use snake_case if you need multiple words.

Each folder needs to contain a file called __init__.py in order to be a regular package rather than just a namespace. Namespaces can be spread over multiple folders in totally different locations. If in doubt, include the __init__.py file. The file can be left blank.

The import statement

A simple import statement looks like this: import example.

This will search for:

  • A subdirectory called example in the current working directory (more on this later)
  • Or a file called example.py in the current working directory
  • Or an installed package called example

Let’s try it out:

  • Open up a folder in VS Code
  • Create a python file, e.g. example_module.py, that contains these lines:
  print('example_module.py is executing')
  foo = 'Hello'
  bar = 'World'
  • Create another file in the same folder
  • Notice that foo and bar are not defined:
print(foo)
# output:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined
  • Add import example_module. Now you can access example_module.foo and example_module.bar.
  • Now add from example_module import foo. This lets you access foo directly instead of including its module name each time.
  • The difference between from x import y and import x is just style. Importing a whole module is often preferable because it’s clearer, but it can lead to long-winded code.
  • Import multiple things at once with commas: from example_module import foo, bar
  • Notice that even if you import the module multiple times, it only printed its message once.
  • You can import everything from a module by using from my_module import * but this is not recommended. It introduces unknown names and makes your code less readable.

Exercise 6.1

For this exercise you’ll need to create a new Git repository. If you need a reminder of how to do this you can revisit Exercise 1 – Your first Python program. Remember to commit your answers often to track your progress in the repository.

Let’s try out some packages from the Python Standard Library. They come installed with Python, but you will need to import each of them whenever you want to use them.

  • Print out the current working directory by using the getcwd function from the os module (documentation here)
  • Print out pi to 10 decimal places, using the math module for the value of pi (documentation here)
  • Print out the current date and time using the datetime module (documentation here)
    • Note that the datetime module contains several types including a datetime type. This is different from the datetime module itself – datetime.datetime is a different object from datetime.
    • Optional: look at the strftime method for how to customise the formatting of the datetime.
Click here for the answers
import os

print(os.getcwd())
import math

print(f'{math.pi:.10f}')
import datetime

now = datetime.datetime.now()
print(now)
print(now.strftime('%d/%m/%y %H:%M'))

Create a package containing some modules:

  • Create a subfolder (package), with two modules inside (module1.py and module2.py) and a blank file called __init__.py.
  • Declare some variables or functions inside those module files.

In the parent folder above the folder called “package”, create a file main.py. This “main” file is going to import those other modules.

To import a module that lives in another package, prefix it with the package name and a dot (not a slash) or import it “from” the package, i.e. do either of the following:

  • import package.module1
  • from package import module1

Try it out! Add import statements to main.py and demonstrate that it is successfully accessing variables or functions defined by module1.py/module2.py.

Info

Note that the syntax continues in this way for folders within folders, e.g. from folder.subfolder.subsubfolder import module. You can still import an object directly with from package.module1 import foo (if module1.py defines something called foo)

If you want to import both, you might try to run import package and then access package.module2 but you will see that doesn’t work. Submodules do not get imported automatically – you must import each one explicitly, e.g. from package import module1, module2.

Automatic import of submodules can be done by including them in the package’s __init__.py file, but we’re not going to look at that now.

What if module1 wants to import module2? The statement you need at the top of module1.py is the same as what you would put in main.py, e.g. from package import module2.

Try it out:

  • In module2, define a variable
  • In module1, import module2 as described above and print its variable
  • In your main.py file, import module1
  • Run the main.py file

Note that you cannot run module1.py directly now. It will fail to find module2. This is because the import statement will search relative to the “current working directory”, which is the directory of the “top level script” (main.py in the example). If you run the module1 file directly, then the current working directory is the package folder, and your import statement would need to be import module2 but then this would not work with main.py.

Relative imports exist: from . import my_adjacent_module will import a file in the same folder or from .my_subpackage import my_module will find something in a subfolder. You might want relative imports within a package of closely related modules so that you can move it around more easily. But in general, stick to absolute imports. The “top level script” (main.py in this example) cannot use relative imports.

Aliases

One final feature of the import statement is using an alias for the imported object.

import datetime as dt

This lets you rename the imported object to whatever you want, if you think it will make the current file easier to read or to avoid conflicts. The above rename of the datetime module lets you define your own variable called datetime without issue.

There are some fairly common aliases for some of Python’s built-in modules, like the above, and for some 3rd party packages, like the NumPy package is usually imported as np. But how to use aliases is down to personal preference (or your team’s preference). Like with most things, consistency will be the most important consideration for readable code.

Exercise 6.2

Create two packages, package1 and package2. Within each package, create a module called my_module. In each of the files called my_module.py, declare some variables. From your main.py file in the root folder, import both modules so that the modules can be accessed directly. For example, what import statements would let you run the following code, assuming each module defined their own example_var variable?

print(my_module1.example_var)
print(my_module2.example_var)
Click here for the answer
from package1 import my_module as my_module1
from package2 import my_module as my_module2

print(my_module1.example_var)
print(my_module2.example_var)

Installing Dependencies

There are many Python packages that are not built into Python but are available on the public PyPi server for you to download and use in your script/project. You can also connect to your own private PyPi server – you might privately publish packages to share between projects, or your organisation might curate a smaller set of packages that are vetted as trustworthy and secure enough for use.

The package manager that is built into Python is a command line tool, pip. If your Python scripts folder was not added to your path, access pip via python -m pip or python3 -m pip on a Mac. Otherwise you should be able to access it directly with pip.

There is thorough documentation available, but here is the core functionality:

  • You can install the latest version of a package with: pip install package-name
  • You can use a version specifier for better control over the version: pip install package-name=1.0.0
  • To generate a snapshot of your currently installed packages: pip freeze. But note that this includes “transitive dependencies”, meaning the thing you installed wants to install something else too.
  • List your dependencies in a file and then install them with: pip install -r requirements.txt. The requirements.txt file should have one package per line, with the same options for versioning as the install command.

It’s worth being aware of how to use pip though we will be using a tool called Poetry (see below) instead of using the pip command line tool directly.

Virtual environments

What if you have two projects on your computer, A and B, with different dependencies? They could even depend on different versions of the same package. How do you reliably run the two projects without your setup for project A affecting project B?

The solution is to give each project its own copy of the Python interpreter and its own library of installed packages. “Python interpreter” means Python itself – the program that reads your .py files and does something with them. This copy of Python is known as a virtual environment. The impact of this on running Python is when you are trying to run project A or update its packages, make sure you are using project A’s copy of Python (i.e. its virtual environment). When running project B, use project B’s virtual environment.

You can manage a virtual environment with Python’s built-in tool called “venv”, but on this course we will be using a 3rd party tool called Poetry (see below).

Poetry

On this course we are going to use Poetry to manage both your project dependencies and the virtual environment to isolate those dependencies from the rest of your system. You should already have this installed (as mentioned in the pre-bootcamp introduction), but if necessary you can follow their installation instructions.

Under the hood it will use pip to install dependencies, but it’s nicer to work with and has some useful features.

The key thing developers expect from a good package manager is to enable repeatable builds:

  • If I don’t deliberately change my dependencies, then I should be able to download the exact same packages every time I build the application.
  • Building it on my computer should reliably be the same as building it on yours.

Updating or adding new dependencies should still be effortless.

Sometimes you install packages that install their own dependencies in turn. Those nested dependencies that you aren’t accessing directly are the transitive dependencies. There could be multiple levels of nesting. Poetry lets us use the same versions of those each build without us having to list them ourselves. pip freeze doesn’t make it clear which dependencies are things you actually care about / which are transitive dependencies.

There will be up to three Poetry files in the top folder of the project.

  • pyproject.toml contains some information about your project in case you want to publish it, as well as a list of your project’s direct depenencies.
  • poetry.lock contains a list of all the packages that were installed last time and exact versions for all of them. This is generate when you use Poetry to install required packages.
  • poetry.toml is an optional file for configuration options for the current project, e.g. whether to create a virtual environment.

Some other tools exist with similar goals such as Pipenv, pip-tools, Conda – but Poetry is coming out on top as a tool that achieves everything we need.

The core commands are:

  • poetry init to add Poetry to a project. This only needs to be done once.
  • poetry add package-name to install a new package and automatically update your pyproject.toml and poetry.lock files as necessary. With just pip, you’d have to run multiple commands to achieve this.
  • poetry install to install everything that the current project needs (as specified by pyproject.toml).
  • poetry show to show your dependencies
  • poetry run ... to run a shell command using Poetry’s virtual environment for the current project. E.g. poetry run python will open up a REPL using the virtual environment’s copy of Python, so the correct packages should be installed. poetry run python my_app.py.

Exercise 6.3

Run through Poetry’s example from their documentation which shows how to add a dependency to a project:

https://python-poetry.org/docs/basic-usage/

Selecting an interpreter in VS Code

For your IDE (such as Visual Studio Code) to read your code correctly when you are installing packages to a virtual environment, it will also need to point to that virtual environment. In VS Code, click on the Python version towards the left of the blue bar at the bottom of the screen to open up the Select Interpreter prompt. Select the correct copy of Python (or type the file path). You can also open the prompt by opening the Command Palette with Ctrl + Shift + P on Windows or Cmd + Shift + P on a Mac. Start typing > Python: Select Interpreter and then select the option when it appears.

To find the location of your Poetry virtual environment’s folder, you can run the command poetry env info. When you “Select Interpreter”, you need to point VS Code to the executable itself – so inside the virtual environment folder, find Scripts/python.exe on Windows or bin/python on a Mac.

Exercise 6.4

The following examples do not currently work. For each one, try to identify the problem(s).

  1. I have two files app.py and shopping_basket.py in the same folder.

app.py:

import shopping_basket

for item in shopping_basket:
  print(f'I need to buy {item}')

shopping_basket.py:

shopping_basket = ['cookies', 'ice cream']
  1. I can successfully run poetry run python src/app.py on command line to run my app in Poetry’s virtual environment. But I get errors when launching the file through VS Code.

  2. I have a file (app.py) in the root folder and two files (shopping_basket.py and price_checker.py) in a subfolder called “checkout”.

    • app.py contains from checkout import shopping_basket
    • shopping_basket.py contains import price_checker
    • When I test the shopping_basket.py file with python checkout/shopping_basket.py, there is no error.
    • When I run python app.py, I get an error ModuleNotFoundError: No module named 'price_checker'. Why?
Click here for answers
  1. The “app” wants to loop over the list declared in shopping_basket.py. Unforunately, the shopping_basket object in app.py refers to the whole module, not the list of strings. Either:

    • change the for-loop to for item in shopping_basket.shopping_basket:
    • or use from shopping_basket import shopping_basket
  2. The most likely issue is you need to select the correct Python interpreter in VS Code. Find the one created by Poetry in a virtual environment.

  3. When you run the file directly, its folder is used as the current working directory. When you run python app.py, all imports including inside shopping_basket.py will need to be relative to that top level file, app.py. So its import statement should be from checkout import price_checker

Alternatively, you can explicitly import it relative to the current file, e.g. from . import price_checker in shopping_basket.py)


Summary

And that’s the end of Chapter 6. You should now be happy with:

  • How to import packages/modules, including different ways of writing the import statement
  • What a Python virtual environment is and why it’s useful
  • The importance of proper package management
  • How to use Poetry to manage the virtual environment and required packages

7. Classes

Chapter objectives

Classes are a very important concept for writing a full-fledged application in Python because they will help keep your code clean and robust.

In this chapter you will learn:

  • What a “class” is and why they are useful
  • What “instance”, “attribute” and “method” mean
  • How to define and use classes
    • Classes can get very complex but we will cover the core features

What is a class?

In Python 3, a class is a type; a type is a class. The only difference is that people tend to refer to the built-in types as “types” and user-defined types as “classes”. There can be more of a distinction in other languages (including Python 2).

For a recap on “types”, refer back to Chapter 3.

Say you define a Dog class. This is where you decide what it means to be a Dog

  • What data is associated with each Dog?
  • What can a Dog do?

Someone’s pet dog is an instance of that class (an object of type Dog). You use the class to construct instances.

Data or functions can be stored on the class itself or on individual instances, but the class generally defines how the object looks/behaves and then you provide specific data to instances.

You could also think of the difference between “class” and “instance” like the difference between a recipe and a cooked meal. A recipe won’t fill you up but it could tell you the necessary ingredients and the number of calories. The Dog class can’t chase a ball, but it can define how dogs do chase balls.

There are multiple reasons classes are useful. They will help you to follow some of the principles of “good” object-oriented programming, such as the five “SOLID” principles. Fundamentally a class does two things:

  • Store data on an object (“attributes”)
  • Define functions alongside that data/state (“methods”)

Defining a class

To define a class in Python, write the keyword class followed by a name for your new class. Put the following code in a new file. Here we will use the pass keyword to say that it’s deliberately empty, similar to how you would define an empty function.

class Dog:
    pass

In a real project you will often put a class in its own module (i.e. file) in order to keep the project organised and easier to navigate. You then import the class in another file to actually use it. E.g. from dog import Dog if you put it in a file called dog.py. These exercises won’t ask you to do so but feel free to try it out for practice.

You create an instance of the class by calling the class like a function – i.e. adding parentheses (). Add this code to your file, under the class definition:

my_dog = Dog()
print(Dog)
print(my_dog)

You should see:

  • Dog is a class (<class '__main__.Dog'>)
  • my_dog is a Dog (<__main__.Dog object at 0x000001F6CBBE7BB0>)
    • The long hexadecimal number is a memory address (it will vary) but it usually won’t be of interest
    • You can define your own text representation for your class, but we haven’t yet.

Class naming convention

In Python, any class names you define should be in PascalCase. This isn’t a requirement, but will help developers read the code. The first letter of each word should be capitalised, including the first, and no underscores should be used. It is sometimes also called “CamelCase” but that is ambiguous – camelCase can have a lowercase first letter.

The different naming convention compared to variables and functions (which use snake_case) should help you keep track of whether you’re handling a class definition or an instance of that class. You can write dog = Dog() and be sure the “dog” variable is an instance and “Dog” is the class itself.

The built-in classes like str, int, datetime are a bit of an exception to this rule.

Attributes

You don’t want to pass around multiple variables (like dog_name, owner, breed etc) for each dog that passes through the system. So our Dog class can be useful simply by grouping those variables together into a single entity (a Dog). Data classes are simple classes that hold data and don’t necessarily provide any functionality beyond that.

This could be achieved with a dictionary but a class is more robust. The class definition is a single source of truth for what it means to be a Dog. You can ensure that every Dog in your system has the same shape. Dictionaries are less reliable – key/value pairs can be added/removed at any point and it’s not obvious where to look to find out what your dictionary must/can contain.

You can also for example make some of the dog’s data constant or optional, by changing the Dog class definition.

Let’s try to make the class more useful by actually storing some data in the object. Python is quite liberal and you can get/set any attributes you want on your instance:

my_dog = Dog()
my_dog.name = 'Rex'
print(f"My dog's name is {my_dog.name}")

Anything stored on the object is known as an attribute. Here, we have added a name attribute to the my_dog object.

If you try to access an attribute that wasn’t set, you will get an error. Try accessing my_dog.foobar and you will see a message like the following:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Dog' object has no attribute 'foobar'

But setting new attributes in this way is not ideal. We want every instance of this class to have the same, predictable shape, i.e. we want to guarantee every dog has a name. We don’t want instances missing an attribute, and we want it to be easy for developers to discover all of the dog’s attributes by reading the class definition. We will now write some code inside the class to achieve that.

Methods

When we put a function on our object, it’s known as a method. Technically it’s also still an attribute, but you will usually stick to calling the variables “attributes” and the functions “methods”.

Define a function called __init__ inside the class. The function name needs to be exactly “__init__”, and then it will automatically be in charge of creating new instances. We no longer want the pass keyword because the class is not empty anymore.

class Dog:
    
    def __init__(self):
        self.name = 'Rex'

Instance methods all have a self parameter which will point to the instance itself. You do not need to call the __init__ method yourself – when you run my_instance = MyClass(), it gets executed automatically. It’s known as the constructor because it is responsible for constructing a new instance.

You can use the self parameter to update the new dog. Try it out: construct a dog, and then print out the dog’s name.

Complete code
class Dog:
    def __init__(self):
        self.name = 'Rex'

dog = Dog()
print(f"My dog's name is {my_dog.name}")

But this still isn’t terribly useful. Each dog should have a different name. So let’s add a parameter to the constructor. The first parameter is always self and then any additional parameters follow. When you construct an instance, only pass in values for the additional parameters.

Complete code
class Dog:
    def __init__(self, name):
        self.name = name

rover = Dog('Rover')
spot = Dog('Spot')
print(f"First dog: {rover.name}")
print(f"Second dog: {spot.name}")

The double underscores in the function name “__init__” indicate that it is a magic method. These are specific method names that get called implicitly by Python. Many other magic methods exist – for example, if you want a nice string representation of your class (useful for logging or debugging), define a method called __str__ that takes self and returns a string.

Exercise 7.1

For this exercise you’ll need to create a new Git repository. If you need a reminder of how to do this you can revisit Exercise 1 – Your first Python program. Remember to commit your answers often to track your progress in the repository.

Define a Notification class that has two attributes:

  • message should be set via a constructor parameter
  • is_sent should be equal to False for every new notification

And then use your class:

  • Create a notification using your class.
  • Print out its message
  • Update its is_sent attribute to True.
  • Create a second notification and check that it has is_sent set to False
Click here for the answer
class Notification:
    def __init__(self, message):
        self.message = message
        self.is_sent = False

notification_one = Notification('Hello, world!')
print(notification_one.message)
notification_one.is_sent = True

notification_two = Notification('Goodbye!')


print(f'Notification One has been sent: {notification_one.is_sent}')
print(f'Notification Two has been sent: {notification_two.is_sent}')

Linking functionality with state

Similar to the __init__ method, you can define any other methods you want by including function definitions indented one level inside the class block. All instance methods should have at least one parameter – “self”.

In Python, you could put all of your dog-related functions in a “dog_functions.py” module. But putting them in the Dog class could be nicer. E.g. imagine you want a function is_healthy_weight that tells you whether an individual dog is healthy based on the data you hold about it. Putting it in a module means you need something like this:

import dog_functions

my_dog_is_healthy = dog_functions.is_healthy_weight(my_dog)

If the function is part of the Dog class, then using it looks like this:

my_dog_is_healthy = my_dog.is_healthy_weight()

Exercise 7.2

Here is a class that stores the contents of your pantry. It holds a dictionary of all the food stored inside.

class Pantry:
    def __init__(self, food_dictionary):
        self.food_dictionary = food_dictionary

The key for each item in the dictionary is the name of the food item, and the value is the amount stored in the pantry. Here is some example usage:

initial_food_collection = {
    'grams of pasta': 500,
    'jars of jam': 2
}
my_pantry = Pantry(initial_food_collection)
  1. Add a method to the Pantry class, called print_contents. It should take no parameters (apart from self). For now, just print the dictionary directly.

You should be able to run my_pantry.print_contents() and see the whole dictionary displayed in your terminal.

Click here for the answer
class Pantry:
    def __init__(self, food_dictionary):
        self.food_dictionary = food_dictionary

    def print_contents(self):
        print(self.food_dictionary)

To use it:

initial_food_collection = {
    'grams of pasta': 500,
    'jars of jam': 2
}
my_pantry = Pantry(initial_food_collection)
my_pantry.print_contents()
  1. Now improve the print message so it’s more human-readable. Print the message “I contain:” and then loop through the food_amount_by_name dictionary, printing the amount followed by the name for each one. For the above example of a pantry, you should get the following result:
    I contain:
    500 grams of pasta
    2 jars of jam

Hint: It will be convenient to loop through the dictionary in this way: for key, value in my_dictionary.items():

Click here for the answer
class Pantry:
    def __init__(self, food_dictionary):
        self.food_dictionary = food_dictionary

    def print_contents(self):
        print('I contain:')
        for name,amount in self.food_dictionary:
            print(f'{amount} {name}')

Use it in the same way as before.

  1. Finally, add another method to the class, called add_food, which takes two parameters (in addition to self), called name and amount. If the pantry doesn’t contain that food item yet, add it to the dictionary. If that food item is already in the dictionary, e.g. you are trying to add some more “grams of pasta”, then add on its amount to the existing record.

E.g. my_pantry.add_food('grams of pasta', 200) should result in 700 grams of pasta stored in the pantry. You can call the print_contents` method to check that it works.

Hint: One way you could do this is using the in keyword to check if a key is already present in a dictionary. For example, 'foobar' in my_dict will return True if that dictionary already contains a key, ‘foobar’.

Click here for the answer
    def add_food(self, name, amount):
        if name in self.food_dictionary:
            self.food_dictionary[name] += amount
        else:
            self.food_dictionary[name] = amount

Alternatively, you can use get to access a dictionary with a fallback value when the key is not present:

    def add_food(self, name, amount):
        self.food_dictionary[name] = self.food_dictionary.get(name, 0) + amount

To use it:

initial_food_collection = {
    'grams of pasta': 500,
    'jars of jam': 2
}
my_pantry = Pantry(initial_food_collection)
my_pantry.print_contents()

my_pantry.add_food('potatoes', 3)
my_pantry.add_food('grams of pasta', 200)
my_pantry.print_contents()

Exercise 7.3

Here is a definition of a class that stores the position and velocity of an object in terms of x,y coordinates.

class MovingThing:
    def __init__(self, x_position, y_position, x_velocity, y_velocity):
        self.x_position = x_position
        self.y_position = y_position
        self.x_velocity = x_velocity
        self.y_velocity = y_velocity

Add a method to it called update_position that has no parameters (apart from self). This should update the object’s position after a unit of time has passed, meaning its x_position should increase by x_velocity and its y_position should increase by y_velocity.

Then create an instance of your class and demonstrate your method works correctly.

Can you make your update_position method take a time parameter instead of always progressing time by “1”?

Click here for the answer
class MovingThing:
    def __init__(self, x_position, y_position, x_velocity, y_velocity):
        self.x_position = x_position
        self.y_position = y_position
        self.x_velocity = x_velocity
        self.y_velocity = y_velocity

    def update_position(self):
        self.x_position += self.x_velocity
        self.y_position += self.y_velocity

moving_thing = MovingThing(0, 10, 3, 1)
print(f'Original position: ({moving_thing.x_position}, {moving_thing.y_position})')

moving_thing.update_position()
print(f'Position after one update: ({moving_thing.x_position}, {moving_thing.y_position})')

moving_thing.update_position()
moving_thing.update_position()
print(f'Position after two more updates: ({moving_thing.x_position}, {moving_thing.y_position})')

To take a time parameter:

    def update_position(self, time):
        self.x_position += self.x_velocity * time
        self.y_position += self.y_velocity * time

Exercise 7.4

Here is a class definition and some data in the form of a dictionary.

class Publication:
    def __init__(self, author, title, content):
        self.author = author
        self.title = title
        self.content = content

dictionary_data = { 
    'title': 'Lorem Ipsum',
    'main_text': 'Lorem ipsum dolor sit amet...',
    'author': 'Cicero'
}
  1. Convert the data from the dictionary to a Publication object
Click here for the answer

Note:

  • The three arguments should be passed to the constructor in the correct order.
  • The dictionary uses the key “main_text” instead of “content”.
my_publication = Publication(dictionary_data['author'], dictionary_data['title'], dictionary_data['main_text'])

You can spread it over multiple lines if you find that easier to read:

my_publication = Publication(
    dictionary_data['author'],
    dictionary_data['title'],
    dictionary_data['main_text']
)
  1. Now here is a list of dictionaries. Convert this list of dictionaries to a list of Publication objects. Imagine that the list could be of any size.
raw_data = [{'title': 'Moby Dick', 'main_text': 'Call me Ishmael...', 'author': 'Herman Melville' }, {'title': 'Lorem ipsum', 'main_text': 'Lorem ipsum dolor sit amet...', 'author': 'Cicero'}]
Click here for a hint Use a for-loop or a list comprehension to perform a conversion on each list item in turn. Here is an example of converting a list, but instead of multiplying a number by two, you want to extract data from a dictionary and construct a Publication.
original_list = [1, 2, 3]

list_via_for = []
for item in original_list:
    list_via_for.append(item * 2)

list_via_comprehension = [ item * 2 for item in original_list ]

Both list_via_for and list_via_comprehension are now equal to [2, 4, 6]

Click here for the answer

Here is the answer achieved two ways.

list_via_for = []
for item in raw_data:
    publication = Publication(item['author'], item['title'], item['main_text'])
    list_via_for.append(publication)

list_via_comprehension = [ Publication(item['author'], item['title'], item['main_text']) for item in original_list ]
  1. It turns out that your application needs to display text of the form “title, by author” in many different places. Add a get_label method to the class, which returns this string.

This is an example of writing a “utility” or “helper” function to avoid duplicated code. You define the logic once and can reuse it throughout the application. It could be written as a “normal” function rather than a method, but this way you don’t have to import anything extra and it’s easy to discover.

Click here for the answer
class Publication:
    def __init__(self, author, title, content):
        self.author = author
        self.title = title
        self.content = content

    def get_label(self):
        return f'{self.title}, by {self.author}'
  1. Loop through your list of Publication objects and print each one’s “label”.
Click here for the answer
for publication in list_of_publications:
    print(publication.get_label())

Exercise 7.5

Define a class to represent users.

  • Users should have name and email_address attributes, both set in the constructor via parameters.
  • Add a uses_gmail method, which should return True or False based on whether the email_address contains “@gmail”.

Hint: Use the in keyword to check if a substring is in a larger string. Eg: 'foo' in 'foobar' evaluates to True.

Click here for the answer
class User:
    def __init__(self, name, email_address):
        self.name = name
        self.email_address = email_address

    def uses_gmail(self):
        return '@gmail' in self.email_address

Troubleshooting exercises

Exercise 7.6

Fix this class definition. There are three issues.

class Dog:
    def __init__(real_age, self):
        age_in_dog_years = real_age * 7

def bark(self):
    print('Woof!')

The fixed class definition should work with the following code and end up printing two lines: Age in dog years: 70 and Woof!.

dog = Dog(10)
print(f"Age in dog years: {dog.age_in_dog_years}")
dog.bark()
Click here for the answer
  • self should be the first parameter of __init__
  • You need to set the age_in_dog_years on the self object. Otherwise, it’s just a local variable inside the function that gets discarded at the end of the function.
  • The bark function should be indented in order to be a part of the class (a method of Dog).
class Dog:
    def __init__(self, real_age):
        self.age_in_dog_years = real_age * 7

    def bark(self):
        print('Woof!')

Class attributes

It is possible to define attributes that belong to the class itself rather than individual instances. You set a class attribute by assigning a value to it inside the class, but outside any function.

class MyClass:
    class_attribute = 'foo'

    def __init__(self):
        self.instance_attribute = 'bar'

You can get it or update it via MyClass.class_attribute, similar to instance attributes but using the class itself rather than an instance.

If you try to access my_instance.class_attribute, it will first check if an instance attribute exists with that name and if none exists, then it will look for a class attribute. This means you can use my_instance.class_attribute to get the value of ‘foo’. But setting the value this way (my_instance.class_attribute = 'new value') will set an instance attribute. So it will update that single instance, not the class itself or any other instances.

This could be useful for a variety of reasons:

  • Storing class constants. For example, all dogs do have the same species name so you could set a class attribute scientific_name = 'canis familiaris'.
  • Tracking data across all instances. For example, keep a count of how many dogs there are in total.
  • Defining default values. For example, when you construct a new dog, maybe you want to set a plays_well_with_cats variable to false by default, but individual dogs could choose to override this.

Exercise 7.7

Create a Dog class with a count class attribute. Every time you construct a new dog, increment this value

Click here for the answer
class Dog:
    count = 0
    
    def __init__(self, name):
        self.name = name
        count += 1

Exercise 7.8

class Cat:
    disposition = 'selfish'

    def __init__(self, name):
        self.name = name

felix = MyClass('Felix')
mog = MyClass('Mog')

felix.disposition = 'friendly'
felix.name = 'Handsome Felix'

Part 1:

After the above code runs, what do these equal?

  • Cat.disposition
  • mog.disposition
  • mog.name

Part 2:

If you now ran Cat.disposition = 'nervous', what would these equal?

  • felix.disposition
  • mog.disposition
Click here for the answers

Part 1:

  • ‘selfish’
  • ‘selfish’
  • ‘mog’

Part 2:

  • ‘friendly’
  • ‘nervous’

Class methods

You can create “class methods” by putting @classmethod on the line above the function definition. Where instance methods have a “self” parameter that is equal to the instance being used, class methods have a “cls” parameter that is equal to the class itself.

class MyClass:
    signature = 'foobar'
    
    @classmethod
    print_message(cls, message):
        print(message)
        print(cls.signature)

The difference between using cls.signature and MyClass.signature inside the class method is that cls can refer to a child class. See the section on inheritance for further information. If in doubt, use cls when you can.

There are also “static methods” in Python (@staticmethod) but we’re not going to look at them now. In short, they belong to the class but don’t have the cls parameter. They should not directly use an instance or the class itself and mainly serves as a way of grouping some functions together within a module.

Inheritance

An important aspect of classes (in almost all languages) is inheritance. Say you’re developing a system that can draw various shapes, though for simplicity we’re just going to print text to the terminal. All of your shapes have some things in common – they have a colour, a position on the canvas, etc.

How do you write that without copying and pasting a bunch of code between your classes?

We can define a base class Shape that all of the different types of shapes inherit from.

class Shape:
    def __init__(self, colour):
        self.colour = colour

    def draw(self):
        print('')

class Dot(Shape):
    def draw(self):
        print('.')

The key part is the (Shape) when we start the definition of Dot. We say that the Shape is a base class or parent class or superclass, while Dot is the derived class or child class or subclass.

When we create a Dot, it inherits all of the behaviour of the parent class, Shape. We haven’t defined an __init__ function for Dot, so it automatically uses the parent class’s one. When we construct a Dot, we have to pass in a “colour”:

my_dot = Dot('black')
print(my_dot.colour)

But if we do define something that the parent already defined, the child’s one will get used. With the example above, that means running my_dot.draw() will print a “.” rather than nothing.

Here is a second child class:

class Rectangle(Shape):
    def __init__(self, colour, height, width):
        super().__init__(colour)
        self.height = height
        self.width = width

    def draw(self):
        print('🔲')

If you want to use a parent method, you can do so via super() as shown above. This is useful when you want to keep some behaviour and extend it.

In this way, your code can show logical relationships between your types of objects (Rectangle inherits from Shape => “a Rectangle is a Shape”), and you can define code in one place that gets used in multiple classes. Python’s classes have more features that we’ve not covered, for example we could use the “abstract base class” module (abc) to declare that the “draw” method needs to be implemented in child classes and not in the “abstract” Shape class. You could read about some features online or when using classes in the course.

Summary

You have now reached the end of chapter 7. At this point you should know:

  • What classes are and why they’re useful
  • How to define a class, including:
  • Defining a constructor
  • Setting instance or class attributes
  • Defining instance or class methods
  • How to construct instances and access attributes/methods
  • How to use inheritance to share functionality between related classes

There are other features of classes in Python that we haven’t touched on, such as:

  • Multiple inheritance (a child class with multiple parents)
  • Abstract classes (using the abc module)
  • The @property decorator
  • The @dataclass decorator
  • “Type annotations” let you benefit even more from classes

8. Flask and HTML basics

Chapter objectives

Python is a very useful tool for helping you write web applications, because you can achieve powerful objectives with very few lines of code. There are a plethora of useful frameworks and tools that you can use, but this chapter will focus on HTML pages with Flask.

In this chapter you will learn:

  • What are the main components of a simple web application,
  • What HTML is and how you can use it,
  • How you can use Flask to set up a simple web server,
  • How you can use Flask to serve HTML pages,
  • How you can handle basic forms and inputs

What is a web application?

In short, a web application is a piece of software that is designed to be accessible from the internet through a browser. All sites on the internet can be considered web applications, some of them more complex than the others. You would access them by typing in a URL in the browser, and then then you would see the resource that is found at that location.

A URL (Uniform Resource Locator) is a reference to a resource accessible on the internet, and the means of accessing it.

For example, https://code.visualstudio.com/docs is a URL composed of the following:

  • A protocol: In this case it’s https (HyperText Transfer Procol Secure), but others exist such as http, ftp, etc. This establishes some standardised rules of communication between the application and the browser.
  • A hostname: In this case it’s code.visualstudio.com. This refers to the domain name of the website or application that you’re visiting.
  • A resource or file name: In this case it’s /docs.

What is HTML?

HTML stands for HyperText Markup Language. As its name suggests, HTML is a Markup Language, not a programming language such as python. A Markup Language refers to a system used for encoding text, which allows inserting special characters to structure the text and alter the way it is displayed.

Exercise 8.1

For this exercise you’ll need to create a new Git repository. If you need a reminder of how to do this you can revisit Exercise 1 – Your first Python program. Remember to commit your answers often to track your progress in the repository.

Let’s try to create a simple web page. You can do that by creating a new file – the name is not important, but the extension must be .html. Within that file, you can just type in Hello World! and save.

Now, if you open the file using any browser, you should see the text Hello World! written in black text on a white background.

HTML Elements and tags

Since HTML is a markup language, it is possible to alter the way the text is structured and displayed. This is done through the use of elements and tags.

You can think of an HTML page as a document, which is composed of several elements. Tags are used to mark the beginning and end of an element, and are usually pre-defined keywords enclosed in angle brackets. To denote the start of an element, you would see a tag such as <tag>, and to mark where the element ends, the tag is accompanied by a forward slash before the keyword, such as </tag>.

Many elements allow other elements to be nested inside them (referred to as child nodes). Child nodes refer to those above them as parent nodes.

Tags are not displayed by the browser. Instead, they instruct the browser on how to display the HTML element.

There are also elements that do not have an end tag and cannot contain child (nested) nodes. These are called void elements, and among those we will use <input> and <img>.

As an example:

  • <html> and </html> are tags
  • <html> Hello World! </html> is an element
  • <input> is also an element

Exercise 8.2

Let’s inspect how the browser views our document. Right click anywhere on the page and then pick “Inspect” (this can have a different but similar name depending on your browser, such as “Inspect Element”). This should open up a panel on the right side. Navigate to the Elements (or Inspector) tab, where you will see the Hello World! text surrounded by some tags.

We’ll discuss what these tags mean shortly, but for now, you can just copy that structure into your html file.

Let’s also add <!DOCTYPE html> as the first line in the file. This isn’t exactly an HTML tag, it is a declaration that instructs the browser on the type of document that it should expect. All HTML files should start with this DOCTYPE declaration.

You will end up with something like this:

<!DOCTYPE html>
<html>
	<head></head>
	<body>Hello World!</body>
</html>

The <html> tag should enclose everything in the document, except for the DOCTYPE declaration.

HTML is not whitespace sensitive. You can use whitespace to format HTML to make it more readable.

Building up the page

Now that you have a very simple HTML page, let’s add some things to it.

Exercise 8.3: Adding a page title

If you hover over the tab in your browser, it should display your file name as its title, e.g. helloworld.html. Since this defaults to the file name, you should change it to something more appropriate. You can do this by adding a <title> element.

You will notice an empty <head> element. This is used by browsers to display metadata (data about data) about the page, such as its title and various other information. You can find more about the Head element here.

Now, pick an appropriate title and add it to the page. Once you save the file, if you refresh the page and hover over the tab in your browser, you should see the updated title.

Click here to reveal the answer

You will need to add a <title>My Hello App</title> element nested under <head>. You should end up with something similar to this:

<head>
    <title>My Hello App</title>
</head>

Adding Text and Headings

The <body> element holds the contents of an HTML document and is a direct child of <html>.

Currently, your <body> element contains only simple text, but all the content that you add to your page (e.g. images, paragraphs, links) should be added under this element.

Text is one of the most relevant types of content that can be added to a web page. You can add plain text to several HTML elements, which will transform it accordingly.

Headings are used to differentiate between different types of texts, offer a logical structuring of the web page, and can be used to provide a common style for your text elements (e.g. fonts, colours, sizes). Essentially, they act as titles and subtitles. They are denoted by <h1>, <h2>, … up to <h6> tags. By default, the text size is the largest with h1, and the smallest with h6, but this can be changed.

There’s a whole theory behind efficiently using headings for Search Engine Optimization (SEO), but some rules of thumb are:

  • Use <h1> for the page’s title. You shouldn’t have more that 1 or 2 <h1> tags, and they should contain keywords relevant for your page
  • Use <h2> and <h3> for subtitles
  • Use <h4>, <h5> and <h6> to structure your text

While the above criteria is intentionally a bit vague and there are no hard rules for the three points, they serve as a guideline for making your page more readable.

Exercise 8.4: Adding some text

Add proper title and subtitle for your page. You should:

  • Add a title such as “Hello from my Python app!”, using an appropriate heading. Note that this is not the page’s title as displayed when you hover over the tab in your browser (i.e. the <title> element) – it’s just a text that will act as the page’s title for your content, a role similar to an article’s title.
  • Add a subtitle such as “Here is my Python Zoo”, using an appropriate heading
Click here to expand an example
<body>
    <h1>Hello from my Python app!</h1>
    <h2>Here is my Python Zoo</h2>
</body>

Images and links are vital for a site’s appearance and functionality. You can add an image using the <img> tag, and a link using the <a> tag.

So far the elements and tags were fairly simple, but these 2 are a bit more complex: they do not work properly unless you also add attributes to them.

HTML attributes act similar to variables, and are used by elements in order to enable certain functionality. They are usually key/value pairs added to the start tag of an element, with the value being enclosed in quotes.

For instance, the <img> element can have the alt attribute, which represents the text that is displayed when the image cannot be loaded. You could create an image element like: <img alt="Alt text for image">.

Each element has a list of attributes that it supports, and each attribute can accept a certain type of value, such as strings, numbers, or pre-defined keywords.

Attributes can also be boolean. If the attribute is present in the element, its value is true. If the attribute is omitted from the element, its value is false.

For instance, an <input> element (e.g. text fields, dropdowns) can be enabled or disabled. <input disabled> is rendered as a disabled field, but <input> (with the disabled field omitted) is active (enabled).

Check out the links above for the <img> and <a> tag, more specifically the “Example” and “Definition and Usage” sections. Then:

  • Add an image of an animal to the directory where your html file is.
  • Link the image in your HTML document, after your subtitle. Don’t forget the alt attribute!
  • Add a link to your page, after the image. Let’s add a link to a google search for an alpaca (you can copy this link). The text that links to the google search should read “Click here to search for an Alpaca”
  • Refresh the page and check that everything works as expected
Click here to expand an example
<img src="alpaca.png" alt="A lovely alpaca image!" />
<a href="https://www.google.com/search?q=alpaca/">Click here to search for an Alpaca</a>

[Stretch goal] Depending on the image you have chosen, it can be a bit too large for your page. Add attributes to the <img> element to resize it to 300 pixels (px) in width and height. You can check the link above for the attributes that you will need to use.

Click here to view the solution
<img src="alpaca.png" alt="A lovely alpaca image!" width="300px" height="300px"/>

Lists

Lists are useful to structure related information. In HTML, there are 2 relevant tags for this: unordered lists (<ul>) and ordered lists (<ol>). These contain several list items (<li>), which are the actual items in the list.

By default, unordered lists will appear with a symbol, such as bullet point, and ordered lists will appear with a number or letter. This can, of course, be changed to any symbol (or even image!).

Exercise 8.6: Adding a list

Below your image and link, add a list (ordered or unordered, up to your preference), of three animals.

Click here to view an example
<ul>
    <li>Octopus</li>
    <li>Lion</li>
    <li>Giraffe</li>
</ul>

Structuring content

So far, you have added each tag one below the other. While this is sufficient for an exercise, in practice you will usually aggregate your content using <div> tags. This has 2 benefits:

  • It will logically separate and aggregate content that is related
  • It can allow easier styling using a language like CSS – this is beyond the scope of this exercise

Similarly, for text-based content, you could use the paragraph <p> tag.

That is all the HTML that we will need for now, but we will come back to it later, to talk about forms, inputs and buttons.

Web Servers

Web servers are applications and hardware that are responsible for delivering content available at a certain URL, when a client makes a HTTP request over the internet.

Both the application responsible for delivering content, the hardware on which it runs, or the system as a whole are called “web server”. It’s usually not important to explicitly differentiate between them, but in this exercise, “web server” will refer to the application that you write.

Let’s think of what happens when you access a site over the internet:

  • You navigate to a URL in your browser
  • This will generate a HTTP GET request (more on this later!)
  • The request will be received by the web server (hardware & software) associated with the domain name (e.g. google.com)
  • The web server will check whether the resource that is requested is mapped to a certain route. In this context, a route is the path after the <hostname>/ on the URL. Looking at the google search example (https://www.google.com/search?q=alpaca/), the resource is mapped to the /search route.
  • If the resource exists, the server responds with a GET Response, which can have many forms. For now, let’s say it’s an HTML page.
  • The browser receives the response and displays it. If it’s an HTML page, it will render the HTML.

The current HTML document is only accessible from your filesystem. Ideally, however, the page should be accessible from the internet, or at least from your machine at a specific URL.

The next goal is to use an existing web server, make it run locally, and customize it in Python to serve your page when accessing its URL in the browser.

Flask

You can use the Flask framework to help with the goals above.

Flask is a framework that provides a built-in web server, as well as a library that allows you to create a web application.

Let’s create a new folder called zoo_app and open it in VS Code.

Exercise 8.7: Adding dependencies

As in previous exercises, you will use poetry to manage python packages

  • Run the command to initialize poetry: poetry init. You can use the default options when answering the prompts.
  • In order to use a virtual environment local to your project, you will need to create a new file called poetry.toml in the root of your project, and add the following two lines to it:
[virtualenvs]
create = true
in-project = true
  • Run the command to add flask to the list of dependencies: poetry add flask.

Minimal flask application

Let’s start by creating an app.py file (it’s important to use this filename!), and paste the following:

import flask

app = flask.Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"
  • The first line imports the flask module
  • Then, it call the Flask constructor with the parameter __name__ to obtain an instance of a flask app. __name__ is a special variable in python that refers to the current module.
  • Then, it adds a route in the app, available at the / path. This means that, when you access the application on /, the hello_world() function will be executed. The / path refers to the homepage, or the base URL where the application can be accessed from (e.g. http://127.0.0.1:5000/).

The function name can be chosen freely since your application’s users will not see it, and there isn’t a convention that needs to be followed as long as it’s a valid python name. However, in practice, it’s useful to choose a brief name that describes what the function does.

Let’s try running the app. In a terminal, type poetry run flask --debug run.

The --debug flag is used by Flask to enable hot reloading, meaning the server will restart when you make a change to your code. This allows you to make changes to your code and have them visible without having to manually restart the flask server.

You should see in the terminal a message similar to:

    Running on http://127.0.0.1:5000

If you visit this in a browser, or http://localhost:5000, you should see “Hello World!”.

localhost refers to the standard domain name of the loopback address. By default, the IPv4 loopback address is 127.0.0.1.

Using your previous HTML file

Since you have already written an HTML file, it would be nice to serve that instead of a hardcoded HTML string.

You will need to:

  • Create a new folder called templates in your project root, and place the HTML file there
    • We’ll find out why this folder is called templates later
  • Create a new folder called static in your project root, and place the animal image there

These folder names are important! Flask will look, by default, in a “templates” folder for HTML files or templates, and in a “static” folder for static files. While this behaviour can be changed, for now it will be sufficient.

Your file structure should look like this:

*   app.py
*   templates/
    *   helloworld.html
*   static/
    *   alpaca.png
*   other poetry-related files

Exercise 8.8: Serving the HTML file

Even though you have placed everything in the relevant folders, you haven’t yet changed the hello_world() function, so you will still get “Hello World!” when visiting the site.

You will need that function to return your HTML file instead (as a string). Flask provides the flask.render_template function to make this easier for you. For now you just need to provide the filename of the HTML document as an argument to this function.

If you refresh the page in your browser and everything is working correctly, you should see a page similar to what you built previously, but with the image not loading.

Click here to view the solution
import flask

app = flask.Flask(__name__)

@app.route("/")
def hello_world():
    return flask.render_template("helloworld.html")

Exercise 8.9

The image is not loading and, instead, you are greeted by the alt text that you have added to the <img> tag. That’s because the path you have used for the src attribute in the HTML file is no longer accurate.

Consider your current folder structure and the fact that you’re running the code from the root of your project (where the app.py file is). What is the new path, from the project’s root to the image file?

Change the src path accordingly, refresh the page, and confirm that the image is loading.

Click here to view the solution

Both ./static/alpaca.png and static/alpaca.png should work.

Flask Templates and Refactoring

Your app can now serve static pages, but if you need to deal with dynamic content such as inputs and forms, this is not enough. Let’s explore how to change the animal list that is currently hardcoded into the HTML file to make it dynamic.

Flask has can use a powerful mechanism called templating. In this context, a template is an HTML file in which you can embed variables, expressions, conditionals, loops and other logic.

The notion of templating is available to many programming languages and tech stacks. While the terminology can vary, the main concepts are very similar.

The templating language that Flask uses is based on Jinja, which allows intertwining code with HTML. You can find more information about it here.

Jinja has a few specific delimiters:

  • {% ... %} is used for Statements (e.g. for loops, if cases)
    • Statements are usually followed by another {% end<statement> %} (e.g. {% endfor %}), to mark where the statement’s block ends
  • {{ ... }} is used for Expressions or Variables

Exercise 8.10

Create a list (you could call it “animals”) of three animal names in app.py. This list should live as long as the server is running.

Where should you place the list?

Click here to view the solution

The list should be placed at the top level of app.py, outside the hello_world function. If the list is a local variable for the function, then it would be destroyed when the function finishes running, so the list would not be persistent.

You can use strings for animal names, and the assignment should look similar to:

animals = ['Octopus', 'Lion', 'Giraffe']

Exercise 8.11

Check out Jinja’s minimal template example here. Make the necessary changes to your HTML file, such that your static list of animals is now being replaced by the animals list in app.py.

You will need to change the way you call render_template, so that the template has access to the variable in app.py. For instance, writing:

render_template("helloworld.html", animals_in_template=animals_in_app_py)

Will allow you to access the variable named animals_in_template in the Jinja template, with the value animals_in_app_py. For the sake of simplicity people often use the same name for both of these variables. In our case we would write:

render_template("helloworld.html", animals=animals)
Click here to view the solution

You will need to use a for loop, to iterate through the animal list and display it within a <li>:

<ul>
    {% for animal in animals %}
        <li>{{ animal }}</li>
    {% endfor %}
</ul>

CRUD, HTTP Methods, inputs and forms

CRUD stands for Create, Read, Update, Delete, which are the four basic operations that a web application would perform.

Thinking of your application so far, you have only implemented the ability to Read information upon visiting the / path. Sometimes this is enough for simple websites that deliver static content. For more interactive user experiences, however, you will likely need to implement other operations.

HTTP (HyperText Transfer Protocol) stands at the foundation of data communication over the internet, as it standardises the way documents are served to clients. It is a request-response protocol based on client-server communication. A typical client could be a browser, which then communicates with servers that deliver HTML files or other media resources.

HTTP defines several operations through which the client communicates with the server. Four of the most common ones are:

  • GET: Is used to request information from the client. It is similar to a Read operation of CRUD, and is intended to have no side effects on the server side.
  • POST: Is used to request the server to perform a certain operation that will possibly modify its resources, such as uploading a message, creating an object in a database, asking to send an email, etc.
  • PUT/PATCH: Used for updating resources.
  • DELETE: Used for deleting resources.

While the standard dictates that HTTP methods behave in a certain expected way, the server’s code ultimately decides what its methods do. Even so, it’s generally good practice to follow these conventions.

This is just a very brief introduction to HTTP and CRUD. These topics will be discused in more detail in further modules.

Exercise 8.12

Although you haven’t explicitly mentioned it in the code, one of the HTTP methods has to be used when calling routes of your application through the browser. Can you guess which method is being used to call your current route?

Hint While the Flask application is running, you can refresh the page and check the terminal in VS Code. The method used, along with the path requested, should appear in the log.

After you’ve figured out the HTTP method, try setting it explicitly in the code. You can do that by adding a methods parameter to the app routing, like this:

@app.route("/", methods=['HTTP_METHOD_GOES_HERE'])

Refresh the page to check that everything is still working.

HTTP Response Codes

Since HTTP is a request-response protocol, the server needs to communicate to the client the status of the request (e.g. whether it was successful or not), in order for the client to interpret the server’s response correctly.

HTTP uses response codes to communicate this. They are composed of three digits, 1xx-5xx, where the first digit signifies the class of responses:

  • 1xx informational response – the request was received by the server
  • 2xx successful – the request was processed successfully
  • 3xx redirect – the client is redirected to another resource or path
  • 4xx client error – the client’s request is invalid
  • 5xx server error – there is an error with the server, when responding to an apparently correct request

Exercise 8.13

Refresh the page and check out Flask’s output in the VS Code terminal.

  • What status code is displayed for the / endpoint?
  • What status code is displayed for the image?
  • Why is the image status code different from the page’s? You can check out here an explanation for the status code that you should be seeing.

Try changing the HTTP operation in your code and refresh the page. What status code are you seeing now and why?

Click here to reveal the answers
  • The status code for the / route should be 200 (OK), signifying that the request completed successfully
  • The status code for the image should be 200 for the first request, but upon refreshing the page it should be 304 (Not Modified). The client already has a copy of the image, so the server instructs the client to use the local copy instead of downloading a new one.
  • If you change the HTTP method in your endpoint to anything else but GET, you should receive a 405 (Method not allowed) response when refreshing the page.

Exercise 8.14

You will need to add a new endpoint to your application, mapping a route called /add. This will be responsible for adding an animal name to the existing list. The method should have a single parameter, which you can assume to be a string.

Consider the following questions:

  • Which HTTP method should you use for this endpoint? The requests will be made with the purpose of adding data to a “database”
  • What status code/action should the function return after adding the animal to the list? If the request is successful, the user should see the (updated) page on /

Once you’ve come up with your answers, double-check with the answers below:

Click here to reveal the answers
  • While we could use any HTTP operation, POST is the most appropriate one considering the conventions
  • Here are two ways of solving the problem:
    • Return the same filled-in template after adding the animal to the list, so the response code should be 200. However, you will notice that, after calling this endpoint, the URL will remain /add instead of /. This can be problematic, because when refreshing the page, the form could be resubmitted, resulting in additional POST requests with unintended consequences!
    • Return a redirect to the / endpoint, so the response code would be one of 3xx. This will instruct the browser to make a request to the URL for /.

If you have a different solution for the second point, feel free to use it as long as it fits the requirements.

Now you should have everything you need to write the endpoint.

Click here to reveal a model implementation
@app.route('/add', methods=['POST'])
def add_animal(animal):
    animals.append(animal)
    return flask.redirect('/')

Forms and input

The new endpoint is currently available in your app, but users cannot interact with it directly. They would, presumably, need to use a 3rd party application to make the requests.

One way of sending a POST request to the app is to add a text input field within a form. Users could fill in the blank space, press the “Send” button, and trigger a request.

Exercise 8.15: Adding a text input and a button

Before or after the list, add a new <input> element of type text. You can see the documentation for that element here.

While it is good practice to also add a <label> to input elements, you can skip that for now

You must add the name attribute to the input element, and the name must match the value that you used in your endpoint as a parameter.

Add a button element below the input element. You can see the documentation here. The button’s type will indicate what the button is supposed to do, so you can go with the type submit. This will submit the form that you will add soon, without the need for additional attributes for the button.

Check that when you refresh the page, there’s a new text input with a button near it. Currently they’re not supposed to do anything.

Click here to reveal the solution
<input type="text" name="animal">
<button type="submit">Send</button>

Exercise 8.16: Adding a form

Add a new form element that will contain the input and button. You can check out the documentation here.

What should be the values for the action and method attributes?

Click here to reveal the solution
<form action="/add" method="post">
    <input type="text" name="animal">
    <button type="submit">Send</button>
</form>

There is one more thing before testing if everything works. You will need to obtain the value of the form’s field in a different way, not as a parameter to the function. So, you should:

  • Remove the function’s parameter
  • Obtain the form’s field from flask.request.form. This is a dictionary containing key-value pairs of your form’s fields, where the keys correspond to the input’s name attribute. So, if your input has the name “animal”, you could obtain the value from flask.request.form['animal']

Test whether everything works

Your application should:

  • Return a page containing a list of animals when accessing the / route
  • Contain a form, with a text input and a submit button
  • Upon submitting the form, the page is refreshed and the list of animals is updated.
For reference, here is a sample solution:

helloworld.html:

<!DOCTYPE html>
<html>
	
	<head>
		<title>My Hello App</title>
	</head>
	<body>
		<h1>Hello from my Python app!</h1>
		<h2>Here is my Python Zoo</h2>
		<img src="./static/alpaca.png" alt="A lovely alpaca image!" width="300px" height="300px"/>
		<a href="https://www.google.com/search?q=alpaca/">Click here to search for an Alpaca</a>
		<ul>
			{% for animal in animals %}
				<li>{{ animal }}</li>
			{% endfor %}
		</ul>

		<form action="/add" method="post">
			<input type="text" name="animal">
			<button type="submit">Send</button>
		</form>
	</body>
</html>

app.py file:


import flask

app = flask.Flask(__name__)

animals = ['Octopus', 'Lion', 'Giraffe']

@app.route("/", methods=['GET'])
def hello_world():
    return flask.templating.render_template("helloworld.html", animals=animals)

@app.route('/add', methods=['POST'])
def add_animal():
    animal = flask.request.form["animal"]
    animals.append(animal)
    return flask.redirect('/')