Programming in Python

Please note this is still under active development. Please contact me if you spot any errors.

by Sam O'Neill

This is an introductory set of units for Python that have been created as the notes for the introductory Programming unit of the undergraduate computing course, 4CM523 - Programming.

It is designed to be reasonably comprehensive and should be read in order. Each unit has a number of lessons each with their own exercise.

I hope you enjoy the course

— Sam

Executing Code and Completing Exercises

If you are taking this course then you will be completing exercises and running code via Visual Studio Code. You will be shown in the labs how to do this. You may see references to Visual Studio Code and main.py in the text, if you prefer to use your own IDE or are comfortable in Python, by all means run the code as you wish.

You can also follow the instructions here.

Alternative ways to run code and do exercises are via the following online Python interpreter - Online GDB Python Compiler.

It is also recommended that you make use of the fantastic Python Tutor. This is an excellent way to view how the code runs step by step. You can also watch a number of excellent videos on their YouTube channel.

Introduction

We will be using the Python programming language in this course.

  • Python is a commonly used beginner programming language since it is considered to be more easily readable and has a simpler syntax than other mainstream languages. Syntax refers to the rules of a programming language; similarly to how grammar refers to the rules of a human language.
  • Python is also a relatively powerful language. It is used to support YouTube, Google, Instagram, Reddit, and other popular software programs.
  • Python has consistently been in the top three most popular programming languages over the past two decades. Other languages that have been consistently popular include Java, C, and Javascript.

In this introductory course, you will be gaining a solid foundation in the Python programming language. The programming concepts you learn will also be helpful for any future programming languages you learn in the future.

How to use this book

You should work through each of the lessons and do the TASK at the end of the lesson.

I would recommend pasting the code snippets into Python Tutor. This is an excellent way to view how the code runs step by step.

You can also refer to the many other books, tutorials and videos found online. An especially useful quick reference is the W3Schools Python Tutorial.

Unit 1 - Getting Started

This unit introduces some things that we need to get up and running. You will learn how to run a basic program, about basic exceptions in Python and about the fundamental data types that Python has to offer - these crucial allow us to handle data in our programs, e.g. storing a number or a persons name.

Hello World

It is customary that the first program that you write in a programming language is the "Hello World" program.

So as not to break this great tradition, let's start our Python journey there!

Python lets you output text to the Terminal.

You can output a piece of text using the print() function (more on functions later in the course).

Normally we will write out python programs in a source file (script) which is where we will write our code. You will need to create and save this file in Visual Studio Code. Make sure it has the .py extension.

1. Hello World!

For now, we can output a piece of text to the terminal by putting the following line into a Python file.

print("Hello World!")

Once you have added this line, hit the Run button at the top of Visual Studio Code.

Run Button

You should then see Hello World! outputted to the terminal.

1.1 Wow! What Just Happened?

When you hit the "Run" button, Python reads your code from the source file and immediately executes it, showing the result in the terminal. Python is an interpreted language, which means that you don’t need to compile your code before running it.

If you would like to execute manually without the run button. Go to the Terminal and type:

python <NAME OF YOUR FILE>.py

This will then convert your code for execution and then run it.

1.2 Tasks

Throughout this course, you'll encounter tasks that you should complete. These are practical exercises designed to reinforce the concepts you've just learned. Some tasks will be more challenging than others, so focus on completing the easier ones before tackling the harder ones.

A TASK will always look like the one given below.

TASKs will normally appear at the bottom of the page


=== TASK ===

Create a Python program in a new file that outputs "Hello World! is cool!" to the Terminal using the print() function.

Comments

In programming, it is important to provide comments in your code to explain what you're doing. This is helpful for both yourself and others who might read your code later.

Comments are not interpreted by Python, so they don't affect how your code runs. Python supports two types of comments:

  • Single Line Comments
  • Multiple Single-Line Comments

1. Single Line Comments

Single-line comments start with a # (hash symbol). Everything following the # on that line will be ignored by Python. For example:

# This is a single-line comment (This won't run).
print("I am learning about comments")

Notice that comments are usually shown in a different color in your code editor.

2. Multiple Single-Line Comments

Python doesn't have a specific syntax for multiline comments. Instead, to comment out multiple lines, you need to place a # at the start of each line. For example:

# This is a single-line comment.
# This is also a single-line comment.
print("I am learning about comments")

If you highlight multiple lines of code and press Ctrl + / (or Cmd + / on macOS), the editor will automatically add a # at the start of each line, turning them into comments.

Note: While you might see triple quotes (''' or """) used for what looks like multiline comments, these are actually docstrings used to document functions or classes. They should not be used for regular comments.

3. A Note on Using Comments

There is some debate about how often to comment, and different programmers have different styles. Here's some basic advice:

3.1 Comment When It’s Useful

For example, this comment is not useful:

# This line prints out Hello World!
print("Hello World!")

Everyone can see what the code does, so this comment doesn’t add value. Only comment when it's not obvious what a piece of code does.

3.2 Be Professional

Your comments reflect on you, especially if others are reading your code. Avoid unprofessional comments, like:

# This code sucks!
print("Hello World!")

or:

# Workaround for Tyrion being a traitor and going off with that dragon lady!
print("Hello World!")

3.3 Keep Comments Brief and Clear

Make sure your comments are concise and to the point. If you need to write a longer explanation, split it into multiple lines. For example:

# The following is a comment that has the job of telling the person reading the source code that this prints Hello World!
print("Hello World!")

would be better as

# Prints Hello World!
print("Hello World!")

or:

# The following is a comment that has the following job.
# To tell the person reading the source code that this prints Hello World!
print("Hello World!")

=== TASK ===

Copy the following code into a new Python file.

# This is a single line comment in Python

print("Replace this line with TASK 1")

print("Hello")
print("World")
print("with Comments")
  1. Modify the code so that the first print statement outputs: Single line comments start with #.
  2. Highlight lines 5-7 and press Ctrl + / (or Cmd + / on macOS) to comment out those lines.

Your final output in the terminal should look like this:

Single line comments start with #

Objects, Types, Operators, and Expressions

To do anything useful in our programs, Python will need to represent data such as numbers, words and booleans. We will learn more about these in Units 2 and 3.

For now we will introduce the most common 4 built-in data types.

1. Objects and Types

In Python, everything you work with is an object. An object can be a number, a word, or any other type of data. Each object has a specific type, which tells Python what kind of data it is and what operations can be done with it.

Python has several built-in data types, which it uses to represent data internally. We'll start by looking at four common ones:

Data TypeDescription
intAn integer. This is a whole number, it can be positive, negative or zero. e.g. 5
floatA decimal number. e.g. 3.14
strText. It consists of individual characters. Strings are enclosed in single quotation marks ' or double quotation marks ". e.g. "Hello World"
boolThe values True or False. Used to make decisions, more in Unit 3

A very useful function in python that we can use is type(). The type() function is a useful tool to check what type of data you're working with. Understanding the type of an object helps you know what operations can be performed on it and how Python will handle it in your program.

x = type(10)
print(x)

Here we are assigning the result of type(10) to the variable x. Basically this stores the result in memory in a name called x. We can then print out what is stored in x.

You could do this in one line like so.

print(type(10))

We will stick to using x as it is more readable.

If you run either of these you will see the output:

<class 'int'>

This is python telling you that 10 is an object of type int. For now, read class as type.

Try it for these other objects.

x = 10.3
print(x)
print(type(x))
x = "This is a string"
print(x)
print(type(x))
x = True
print(x)
print(type(x))

This first gets the type of the object, here a str and then passes that to the print() function.

2. Operators and Expressions

We can combine objects with operators to form expressions. When evaluated, these expressions produce a new object.

For example, we can combine the objects 10 and 5 with the + operator,

x = 10 + 5              # new object 15 assigned to x
print(x)                # prints out 15
print(type(x))          # prints out <class 'int'>

to create a new object 15 of type int.

All built-in data types have operators that we can use to form expressions.

For example, we can test if two expressions are equal with the == comparison operator.

x = 10 + 5 == 3 * 5     # assign the result of comparing 10 + 5 and 3 * 5. 
print(x)                # prints out True
print(type(x))          # prints out <class 'bool'>

This will return True. Python knows how to test whether two numbers are equal and returns you a new object of type bool.

We will cover operators for numbers, strings, and bools in their respective lessons in this unit.

3. Help Function

The help() function gets information about an object (it can also be used for other things.)

Try typing the following into the terminal or a Python file:

help(str)

You will need to click in the terminal and press Ctrl + c to exit this.

You can also type python into the terminal to enter the Python Interative Shell and then type help(str) and press Enter. You can open this by typing python into a terminal.


=== TASK ===

Create a new Python file.

Edit the file to output the type of the following expressions. You will need to use the print() and the type() function.

Make sure you have read all of the above before attempting this (especially the end of Section 1).

12 + 5
5 + 3.0
12 / 5
12 + 5 == 5 * 3.0

The output of your program should be:

<class 'int'>
<class 'float'>
<class 'float'>
<class 'bool'>

Basic Exceptions

Every time that you try and run your code, Python will first try to parse your code, if it encounters something that it does not recognise, it will raise an exception.

This is a warning to the coder that they have done something invalid and the program will not run. Please read the exception and try to understand it.

The three main exceptions you may encounter for now will be:

  1. SyntaxError
  2. NameError
  3. TypeError

1. SyntaxError

A SyntaxError is perhaps the most common kind of complaint you get while you are still learning Python.

It means you have entered something that Python does not understand, this is commonly a spelling mistake or something you have missed.

NOTE: Always pay attention to the error message, it is telling you what is wrong with your code!

Try running the following code:

print(hello world)

You will get the following SyntaxError because of some missing "" around the string.

  File "<stdin>", line 1
    print(hello world)
                ^
SyntaxError: invalid syntax

2. NameError

A NameError occurs when a local or global name is not found. This refers to variables, functions, and other things like modules and classes.

Basically, Python reserves particular words such as print.

Try running the following code:

printf("Hello World!")

You will get the following NameError because we have misspelled print.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'printf' is not defined

3. TypeError

A TypeError occurs when the data types of objects in an operation are invalid. For example, trying to divide a number by a string.

Try running the following code:

100/"5"

You will get the following TypeError because we are trying to divide / an int by a str. These two types don't play together well!

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'int' and 'str'

=== TASK ===

  1. Copy the following code into a new Python file.
# You need to fix the following lines
# Run the code and then use the error messages to fix each line

# This line has a SyntaxError
print(hello world)

# This line has a NameError
printf("Hello World!")

# The following lines cause a TypeError
int1 = 100
str1 = "10"
print(int1 / str1)
  1. You will see a SyntaxError on line 5 because of some missing "" around the string.
print(hello world)

Fix this so that it prints out hello world.

  1. Once you have completed this run the code. You will see a NameError on line 8 because printf is not a valid name.
printf("Hello World!")

Fix this so that it prints out Hello World.

  1. Once you have completed this run the code. You will see a TypeError on line 13 because we are tring to divide / an int by a string.
int1 = 100
int2 = "10"
print(int1 / int2)

Fix this so that it prints out 10.0.

References

Python Exceptions

Indentation, WhiteSpace, and Blank Lines

Python is unusual in that it requires indentation for lines of code. This will make more sense when we do if statements, while loops, and for loops.

For now, all you need to know is:

  • Do not put whitespace (spacing or tabs) in front of a line of code.
  • Python doesn't care about blank lines.

=== TASK ===

Copy the following code into a new Python file.

# This line is not indented properly. Fix this
  print("Indentation matters in Python")

# You will notice that we have blank lines
# Python doesn't care about these!

# There is extra whitespace inside the print statement 
# Python doesn't care about this
print("Hello World!"  )  

Run the code, and you will see an IndentationError exception on line 2.

This is due to the whitespace at the beginning of the line.

Fix this.

NOTE: You may also see that the code has a red squiggly line. This is highlighting an error!

The Hello User Program

This is a mini-program and is presented to help you understand a concept. This is a very simple program that asks the user to input their name and their age.

It then says hello back to the user.

You will need to create a new file and paste this in to have a play.

name = input("Hello, what is your name?\n")
age = input("What is your age?\n")
print(f"Hello {name}, your age is {age}.")

Key Takeaways

We see 3 things here that are of interest:

  1. The input() function. This asks the user for some input via the terminal.
  2. We assign (=) the input to a variable (name and age)
  3. We print back out to the user using an f-string.

The next lesson on Simple I/O (Input/Output) will explain these in more detail.

Simple I/O (Input/Output)

For most of this course, we will work with the terminal. For your programs to be of more use, we need to have a way for the user to interact with them.

1. User Input

You can ask the user for input via the terminal using the input() function.

For example,

input("Please enter something:\n")

will print out Please enter something and then a blank line (this is because of the \n escape character in the string).

The user can then enter something. Try this in a Python file.

2. Storing User Input

The above code does not store the input that the user enters. To do this you need to assign it to a variable. Update the Python file as follows:

input_string = input("Please enter something:\n")
print(f"You entered - {input_string}")

Now the input is stored in the variable input_string and we can use it in our program.

You can think of input_string as a box that stores the input from the user. We can then get the contents of the box at different points of our program.

The line,

print(f"You entered - {input_string}")

uses a Python f-string, which just lets you put the contents of the variable into the string. Here we put the contents of input_string in between the curly braces {}.

We will discuss variables and f-strings a lot more in the next unit.

3. Casting the Input

Whenever you get input from the user, it will be of type str. Sometimes we wish to convert this to a number or other type. To do this we can cast the variable to another type. We can cast to an int using the int() function.

input_string = input("Please enter a number:\n")
x = int(input_string)
print(f"{x} + 5 = {x+5}")

The above code asks for a number, then casts the str to an int and then prints the result of adding 5 to the number.

Note that the line print(f"{x} + 5 = {x+5}") places the contents of x into the curly braces.

What happens if you don't enter a number? Copy the code into a Python file and have a play with this.

NOTE: We could have done the casting in one line.

x = int(input("Please enter a number:\n"))
print(f"{x} + 5 = {x+5}")

=== TASK ===

Create a new Python file.

Create a simple program that asks the user for a number and then prints out 10 times that number.

If the user enters 3 the program should work as follows:

Please enter a whole number:
3
3x10 = 30

If the user enters 10 the program should work as follows:

Please enter a whole number:
10
10x10 = 100

I have seen students try to solve this with an if statement (we haven't even introduced this yet!). Please don't do this, your program should handle any number and print out the result of multiplying it by 10. The two examples above are just two particular cases.

HINT: You will need to cast the input to an int.

Unit 2 - Basic Elements of Python

This unit looks at the basic elements of Python. It is really important that you get a good handle on these elements. That does not mean you have to memorise them, but you should understand them.

Programmers regularly switch languages and look up things. Your secret programming weapon is Google! You will find almost unlimited resources on Python, but knowing the right words to use in a search will certainly help you.

The lessons in this unit are:

  • Variables and Assignment
  • Statements vs Expressions
  • Numbers
  • Strings
  • Casting
  • String Indexing and Slicing
  • String Methods

Variables and Assignment

Variables are one of the most important things in programming languages. They allow us to assign a name to an object so we can store it for use later on in our program.

1. Assignment

We assign an object to a variable using the assignment operator =. For example,

pi = 3.14

assigns 3.14 to the variable named pi.

We can then use the variable in our programs. Consider the following program,

pi = 3.14
radius = 6
circumference = 2 * pi * radius
print(circumference)

this will print the value of the circumference of the circle with radius 6.

The point is that the line circumference = 2 * pi * radius now used the variables pi and radius to calculate the value of the circumference.

The following image illustrates the binding of the variables to the objects in memory. Note it isn't really how Python works internally, but it is good enough as a mental picture.

Variable Assignment

2. Reassignment

Python lets you reassign a variable to another object.

The following code,

pi = 3.14
radius = 6
circumference = 2 * pi * radius
print(f"The circle with radius = {radius} has circumference = {circumference}")

radius = 0

print(f"The circle with radius = {radius} has circumference = {circumference}")

will output,

The circle with radius = 6 has circumference = 37.68
The circle with radius = 0 has circumference = 37.68

because we reassigned the variable radius to the value 0, but we did not recalculate the circumference.

The following depicts the reassignment of the variable radius. Variable Reassignment Image reproduced from Chapter 2: Introduction to Computation and Programming Using Python.

3. Objects in Memory

Python stores objects in memory and when you assign a variable it binds that variable to the location of the object in memory.

You can find the unique id of an object by using the id() function. This refers to the object's memory address and will be different each time you run the program.

Input the following two lines into the terminal, remember to press enter after each line:

pi = 3.14
id(pi)

For now the id() function is a bit of a novelty. We will see later on that it can prove essential in debugging programs when we are editing mutable (changeable) types like lists.

Note that two objects can have the same id if one object has been removed from memory (Python automatically cleans up objects that are no longer used). Essentially the unique id is being re-used.


=== TASK ===

This should be relatively simple, consider it a freebie.

Copy the following code into a new Python file.

pi = 3.14
radius = 6
circumference = 2 * pi * radius
print(f"The circle with radius = {radius} has circumference = {circumference}")

# Reassign radius to the value 15
# Reassign circumference to the circumference of the circule with radius 15
# In addition to the existing print statement, print out the new circumference.

  • Reassign radius to the value 15
  • Reassign circumference to the circumference of the circule with radius 15
  • In addition to the existing print statement, print out the new circumference.

The output of the program should be:

The circle with radius = 6 has circumference = 37.68
The circle with radius = 15 has circumference = 94.2

Statements vs Expressions

So far you have already seen examples of both statements and expressions and your Python programs will be made up of both of these.

We will give a basic definition of the two, however, the real story is much more complicated.

1. Statements

Statements instruct Python to do something, that is the Python interpreter can execute the statement. To date, you have seen two types of statements: print and assignment.

For example,

print("Hello World!")

is a statement that instructs python to print the string "Hello World!" and,

int1 = 1

is a statement that instructs Python to assign 1 to the variable int1.

2. Expressions

Expressions differ from statements in that they evaluate to an object. They are a combination of objects and operators.

For example,

3 + 5 

combines the integers (objects of type int) 3 and 5 with the operator + and evaluates to the integer 8.

This example,

True and False

combines the boolean (objects of type bool) True and False with the logical operator and and evaluates to the boolean False.

3. Combining Statements and Expressions

Most of your code will tend to be a statement made up of expressions.

For example, you can assign the result of an expression to a variable. Consider the following statement (assignment)

a = 3 + 5 

Here 3 + 5 is an expression and the result, 8, is assigned to the variable a. The whole line is a statement that contains an expression.


=== TASK ===

A nice simple one.

Create a new Python file.

Print Statements instruct Python to do something

Print Expressions combine objects and operators and evaluate to an object

Print Statements can include expressions


Numbers

Python has three numeric types:

  • int
  • float
  • complex

Unless you are doing something specifically mathematical you will only use int and float. So we will cover just these in this lesson.

1. ints and floats

The following table provides a summary of int and float.

Data TypeDescription
intAn integer. There is no upper or lower limit to how high or low it can be (Python 3).
floatA decimal number. A double-precision floating-point number, equivalent to double in many other programming languages.

1.1 int

An int (integer) is simply a whole number such as 5 or -100. If you assign a whole number to a variable, python will automatically know that the type of object is an int.

Type the following two lines in the terminal:

a = 10
type(a)

You will see that the output confirms that a is of type int.

1.2 float

An float (floating point number) is a number containing one or more decimals, such as 5.3 or -100.43. If you assign a decimal number to a variable, python will automatically know that the type of object is an float.

Type the following two lines in the terminal:

a = 10.6
type(a)

You will see that the output confirms that a is of type float.

You can find out the minimum and maximum float using

import sys
print(sys.float_info.min)
print(sys.float_info.max)

On the Repl.it systems it is -2.2250738585072014e+308 to 1.7976931348623157e+308. Which is approximately -2.23*(10**308) to 1.80*(10**308)

That is astronomically big!

1.3 Dynamic Typing

You can combine an int and a float. Python will internally understand to convert the int to a float so that you can combine two floats.

For example, type the following into the terminal,

a = 1 + 3.3
print(a)
type(a)

the first line is adding an int and a float which results in an object of type float with the value 4.3.

2. Arithmetic Operators

Number objects can be combined with the following arithmetic operators.

OperatorNameExample
+Additionx + y
-Subtractionx - y
*Multiplicationx * y
/Divisionx / y
%Modulusx % y
**Exponentiationx ** y
//Floor Divisionx // y

All of +, -, * and / will be familiar to you. However, %, ** and // may not be.

2.0.1 Modulus (%)

The modulus operator is very useful for testing if a whole number is divisible by another whole number. It is an implementation of modulo (clock) arithmetic in mathematics.

If a number is divisible by another the output of x % y will be 0 (x and y can be positive or negative).

10 % 3 # output is 1 (not divisible)
10 % 2 # output is 0 (divisible)
14 % 4 # output is 2 (not divisible)
14 % 7 # output is 0 (divisible)

You will see this in computational mathematics, for more info look at Modulus Operator - Real Python

It can also be used to find the remainder, but be careful, this doesn't work in all circumstances. I would always use math.remainder().

import math # imports extra maths stuff
print(math.remainder(10, 3)) # remainder of 10/3 is 1
print(10 % 3) # output is 1. (% WORKS)

print(math.remainder(-10, 3)) # remainder of 10/3 is 1
print(-10 % 3) # output is 2. (% DOES NOT WORK)

print(math.remainder(10, -3)) # remainder of 10/3 is -1
print(10 % -3) # output is -2. (% DOES NOT WORK)

print(math.remainder(-10, -3)) # remainder of 10/3 is -1
print(-10 % -3) # output is -1. (% WORKS)

So it works when both numbers are positive or both numbers are negative.

My advice is don't use it for the remainder.

2.0.2 Exponentiation (**)

The exponentiation operator ** takes the number x (called the base) to the power of y (called the exponent).

2**3 # Evaluates to 8 (i.e. 2*2*2)
2**4 # Evaluates to 16 (i.e. 2*2*2*2)
5**2 # Evaluates to 25 (i.e. 5*5)
5**3 # Evaluates to 125 (i.e. 5*5*5)

2.0.3 Floor Division (//)

The floor division operator // takes the number x/y and rounds it down.

For example,

10/4 is 2.5 rounded down is 2. So,

10//4 # Evaluates to 2

-10/4 is -2.5 rounded down is -3. So,

-10//4 #Evaluates to -3

2.1 Order of Operations

When using arithmetic operators you need to be aware of the order in which Python evaluates an operator. This is known as the operator precedence.

For example,

3 + 5 * 2

is 16 right? If you enter this into the terminal you will get 13. Well done if you spotted this.

This is because the multiplication * operator has higher precedence than the addition + operator. Python is doing the following

# This is not code, we are manually evaluating to see how Python works with this expression

3 + 5 * 2     # (Evaluate 5 * 2)
3 + 10        # (Evaluate 3 + 10)
13            # (Final Object)

If you want to force the 3 + 5 to be evaluated first, then you need to use parentheses.

(3 + 5) * 2

This now evaluates to 16. Try it in the terminal.

The following table gives the arithmetic operator precedence. Higher entries have higher precedence.

OperatorName
()Parentheses
**Exponentiation
*, /, %, //Multiplication, Division, Modulus, Floor Division
+, -Addition, Subtraction

2.2 Left-to-right Evaluation

You will notice from the table that some operators have the same precedence as each.

What happens then with the following:

5 - 2 + 1

If you try this in the terminal you will get the answer 4. However, this could have been read as either 3 + 1 or 5 - 3 depending on whether you evaluated the - or + first.

Python follows the left-to-right convention. That is, if two operators are of the same precedence, then Python will evaluate the leftmost first. Hence:

# This is not code, we are manually evaluating to see how Python works with this expression

5 - 2 + 1     # (Evaluate 5 - 2)
3 + 1         # (Evaluate 3 + 1)
4             # (Final Object)

3. Comparison Operators

You can also compare two numbers and they will result in a bool object. Either True or False.

OperatorNameExample
==Equalx == y
!=Not equalx != y
>Greater thanx > y
<Less thanx < y
>=Greater than or equal tox >= y
<=Less than or equal tox <= y

For example, the following expressions evaluate to:

ExpressionResult
3 < 5True
3 > 3False
3 >= 3True
3 == 5False
3 != 5True

3.1 Order of Precedence

All the comparison operators given above have lower precedence than the arithmetic operators.

OperatorName
()Parentheses
**Exponentiation
*, /, %, //Multiplication, Division, Modulus, Floor Division
+, -Addition, Subtraction
==, !=, <, >, <=, >=Comparison Operators

Therefore something like the following expression,

3 + 5 == 3 * 5

is evaluated as follows:

# This is not code, we are manually evaluating to see how Python works with this expression
3 + 5 == 3 * 5   # Evaluate 3 + 5
8 == 3 * 5       # Evaluate 3 * 5
8 == 15          # Evaluate 8 == 15
False

Note: I would always want to convey my intention and not rely on the order of precedence, it is easy to forget. So I would rewrite the above as:

(3 + 5) == (3 * 5)

This is identical, but it tells the reader more explicitly that the stuff in the parentheses is evaluated first.


=== TASK ===

Copy the following code into a new Python file.

# DO NOT TOUCH THE FOLLOWING LINES 4, 5 and 6
# THESE ARE USED FOR THE INPUT. 

a = int(input("Please enter a number: ")) # leave this line alone
b = int(input("Please enter a number: ")) # leave this line alone
c = int(input("Please enter a number: ")) # leave this line alone

# TASK 1. 
# print out the type of 10.3 + 5

# TASK 2.
# print(a + b * c) # Correct this so that the ``+`` is evaluated first
# print(a**b+c)    # Correct this so that the exponent is b+c

# TASK 3. 
# print the output of a < b

# TASK 4. 
# print the output of (a < b) == (a <= b)

This reads in three whole numbers and stores them in variables a, b and c. Try running the code and it will ask you for three numbers.

  1. Print the type of 10.3 + 5

  2. Fix the following two lines:

print(a + b * c) # Correct this so that the ``+`` is evaluated first
print(a**b+c)    # Correct this so that the exponent is b+c
  1. Print the result of a < b

  2. Print the result of (a < b) == (a <= b)


If you enter 2, 3 and 2 into your program then a=2, b=3 and c=2. If you have made the above changes correctly, your program should output:

<class 'float'>
10
32
True
True

References

W3schools Python Operators

Precedence and Associativity of Operators in Python

Strings

This lesson introduces you to the basics of strings in Python.

Python has a built-in datatype str known as a string, you can think of this as a piece of text.

1. Basic Printing of Strings

Strings can be printed to the terminal using the python print() function (we will explore functions in detail later in the course).

For example:

print("hello world!")

Prints hello world! to the terminal.

Whatever is contained between the brackets will be printed out to the terminal using double quotes "".

2. Quotes

In Python strings can be enclosed in either single quotes '...' or double quotes "..."

For example:

print('hello world!')

Also prints hello world! to the terminal as per section 1.

NOTE: Although we can use both, please stick to double quotes as most languages represent strings with double quotes!

3. Escape Characters

My first program was "Hello World!" in Python

Try the following in the terminal.

print("My first program was "Hello World!" in Python")

You will see a SyntaxError: invalid syntax, this is the Python interpreter telling you that it does not understand the code you have entered. If you look closely you will see that it is pointing to the H, the character after the second "". This is because it is illegal to put a double quote character " within a string.

To fix this we need to use an escape character, in Python, this is the \ (backslash) followed by a particular character.

Amend the previous line of code to include backslashes for the quotes in the string.

print("My first program was \"Hello World!\" in Python")

If you run this you will now get the desired output My first program was "Hello World!" in Python.

There are a number of special escape characters in Python, you can look them up. We will just look at the following:

NameEscape Character
tab\t
new line\n
backslash\\
single quotation mark\' (You only need to use this when the string in enclosed is single quotation marks.)
double quotation mark\" (You only need to use this when the string in enclosed is double quotation marks.)

For example,

print("My first program was \"Hello World!\" in Python\n\nPython is cool!")

prints out :

My first program was "Hello World!" in Python
  
Python is cool!

4. Multiline Strings

Multiline strings let us write multiple lines in a string without escape characters or multiple print statements. In Python, they start with ''' or """ and end the same way.

numbers = '''3
5
6
7
'''  
print(numbers)

The above will print out the following to the terminal.

3
5
6
7

If you have an escape character in your multiline string and you want this to appear in the terminal you should escape it with another \.

For example,

print('''This is a multiline string.

The tab escape character in python is \t.
''')

will result in the output:

This is a multiline string.

print('''This is a multiline string.

The tab escape character in python is   .
''')

i.e. it will put a tab into the output. To get \t to show up you need to escape it with another \ as follows:

print('''This is a multiline string.

The tab escape character in python is \\t.
''')

NOTE: This is the same in a normal string. To output an escape character as text you should escape it!

5. Concatenating Strings

To concatenate, or combine, strings you can use the + operator.

For example,

str1 = "Hello"
str2 = "World!"

print(str1 + str2)

results in the output:

HelloWorld!

To get the standard Hello World! we could do the following.

str1 = "Hello"
str2 = "World!"

print(str1 + " " + str2)

Note that you can also just concatenate strings within the print function:

print("Hello" + " " + "World")

6. Formatting Strings

We can also combine strings using the format() method.

The format() method takes the passed arguments and puts them in the string where the placeholders {} are:

For example,

str1 = "string 1"
str2 = "string 2"
print("Hello {} and hello {}.".format(str1, str2))

results in the output:

Hello string 1 and hello string 2.

We can also achieve this using index numbers starting at {0}

str1 = "string 1"
str2 = "string 2"
print("Hello {0} and hello {1}.".format(str1, str2))

This is especially useful if you wish to repeat strings stored in variables.

For example,

str1 = "string 1"
str2 = "string 2"
print("Hello {0} and hello {1}.\nBye {0} and bye {1}.".format(str1, str2))

results in the output:

Hello string 1 and hello string 2.
Bye string 1 and bye string 2.

7. f-Strings

Since Python 3.6 you can use f-Strings (formatted string literals), we will use these throughout the course, but you should understand how to use the others in case someone else has written code using them.

We can achieve the previous output by doing the following:

str1 = "string 1"
str2 = "string 2"
print(f"Hello {str1} and hello {st2}.\nBye {str1} and bye {str2}.")

which again results in the output:

Hello string 1 and hello string 2.
Bye string 1 and bye string 2.

Note that in front of the string we put an f. This tells python that anything in curly braces should be evaluated. So in the example above, the values of str1 and str2 are substituted into the string.


=== TASK ===

Create a new Python file.

  1. Print Programming in Python to the terminal.  

  2. Print Programming in Python with single quotes to the terminal using single quotes ''.

  3. Print the following to the terminal.

I know how to put "quotes" into a string.
  
And put in new lines!
  1. Using a multiline string print
I am using a multiline string to:
- print multiple lines 
- without the use of the escape character \n

to the terminal.

  1. Create the following strings and store them in the variables str1 and str2
str1 = "string 1"
str2 = "string 2"

Print Hi, I am string 1 and I am string 2! to the terminal using string concatenation.

  1. Print string 1 is before string 2 and string 2 is after string 1 to the terminal using f-strings.

The entire output of the program should be as follows:

Programming in Python
Programming in Python with single quotes
I know how to put "quotes" into a string.

And put in new lines!
I am using a multiline string to:
- print multiple lines
- without the use of the escape character \n
Hi, I am string 1 and I am string 2!
string 1 is before string 2 and string 2 is after string 1

Casting

It is very common that you will need to convert one data type to another.

Type the following into the terminal:

print("The meaning of life is " + 42)

You will the following TypeError:

TypeError: can only concatenate str (not "int") to str

This is because you are trying to add a str and an int. Python does not know how to do this. To correct this you need to do something called casting.

The following code casts (converts) the int to a str using the constructor function str():

print("The meaning of life is " + str(42))

If you try this you will see that it works.

Type str(42) into the terminal on its own and you will see that it returns the str '42' (note it uses single quotes, but that is the same as double quotes in Python!).

Note that we could have done the following:

print(f"The meaning of life is {42}")

This is because the python f-string knows how to convert the number 42 to play nicely with the string. Try it in the terminal.

1. How to Cast

Casting in python is therefore done using constructor functions:

FunctionDescription
int()Constructs an integer number from an integer, a float (by removing all decimals), or a string (providing the string represents a whole number)
float()Constructs a float number from an integer, a float, or a string (providing the string represents a float or an integer)
str()Constructs a string from a wide variety of data types, including strings, integers, floats, and booleans
bool()Constructs a boolean from a wide variety of data types, including strings, integers, floats, and booleans

1.1 Examples

Try these in the terminal:

int(1)           # Creates an int with value 1
int("2")         # Creates an int with value 2
int(4.3)         # Creates an int with value 4
int(True)        # Creates an int with value 1
int(False)       # Creates an int with value 0
float(1)         # Creates a float with value 1.0
float("2")       # Creates a float with value 2.0
float("3.142")   # Creates a float with value 3.142
float(4.3)       # Creates a float with value 4.3
float(True)      # Creates a float with value 1.0
float(False)     # Creates a float with value 0.0
str(1)           # Creates a str with value '1'
str("3.142")     # Creates a str with value '3.142'
str(4.3)         # Creates a str with value '4.3'
str(True)        # Creates a str with value 'True'
str(False)       # Creates a str with value 'False'

bool() can throw up some unexpected results unless you understand what it is doing.

You might think that bool("0") would result in False, it doesn't!

Python treats everything as True other than False, 0, an empty string "" and some other things we have yet to encounter, empty lists, dictionaries, tuples, and the None keyword which represents no value at all (more on that later).

bool(1)          # Creates a bool with value True
bool("3.142")    # Creates a bool with value True
bool(4.3)        # Creates a bool with value True
bool("0")        # Creates a bool with value True
bool(True)       # Creates a bool with value True
bool(False)      # Creates a bool with value False
bool(0)          # Creates a bool with value False
bool("")         # Creates a bool with value False
bool([])         # Creates a bool with value False
bool({})         # Creates a bool with value False
bool(())         # Creates a bool with value False
bool(None)       # Creates a bool with value False

=== TASK ===

Create a new Python file and write a program that outputs the following to the terminal for a given X and Y. The result of multiplying X by Y is X*Y

For example, for 2.1 and 3:

Please enter a number:
2.1
Please enter another number:
3
The result of multiplying 2.1 by 3.0 is 6.3

For example, for 5.2 and 3.4:

Please enter a number:
5.2
Please enter another number:
3.4
The result of multiplying 5.2 by 3.4 is 17.68

References

W3Schools - Python Casting

The What's My Age Again (in Hours)? Program

The following program is named after the pop-punk classic by Blink 182 - What's My Age Again off the album Enema of the State.

The program asks the user for the hour, day, month, and year of their birth.

It then outputs the person's age in hours.

Here is a plan for our program.

In pseudocode:

Ask the user for the hour, day, month and, year of birth

Compute the difference between the current date and time and the user's birth

Convert the difference into hours

Print out the user's age in hours

Please try to understand how this program works and match it with the pseudocode above.

Copy and paste this code into a new file to play around with it.

1. The Complete Program

import datetime

hour = int(input("What hour (24hr) were you born?\n"))
day = int(input("What day (number) of the month were you born?\n"))
month = int(input("What number month were you born?\n"))
year = int(input("What year were you born?\n"))

# create a new datetime object with user input
birth_datetime = datetime.datetime(year, month, day, hour)
# get current datetime
current_datetime = datetime.datetime.now()
# compute the difference
diff = current_datetime - birth_datetime

# convert days (*24) and seconds (/3600) to hours and sum 
hours = diff.days*24 + diff.seconds/3600

print(f"Your age in hours is {round(hours)}.")

How would you estimate that the output is actually correct?

You should try and do a calculation to convince yourself that this works properly.

String Methods

Python has a number of built-in methods that you can use on strings to do simple transformations.

You can see a comprehensive list via the link in the References.

1. What is a Method?

A method is something you can call on an object that does something with that object. This will make more sense when we get to object-orientation later in the course.

For now, if we have a string we can call a method using the . notation and we will get back a new string.

For example, type the following into the terminal,

"hello world".upper()

returns the new string:

"HELLO WORLD"

Here we called the upper() method on the str object "hello world" and it gave us back the str "HELLO WORLD".

1.1 Be Careful

String methods return new strings, consider the following code:

a = "hello world"    # (assign "hello world" to variable a)
a.upper()            # (call upper() on variable a)
print(a)             # (print a)

will output

hello world

This is because it returns a new string, it does NOT change the original string. So a is left as "hello world".

We would either have to do the following to print out the upper case version,

a = "hello world"    # (assign "hello world" to variable a)
print(a.upper())     # (print out the result of calling upper() on variable a)

or,

a = "hello world"    # (assign "hello world" to variable a)
b = a.upper()        # (call upper() on variable a and assign result to variable b)
print(b)             # (print b)

1.2 Familiarise Yourself

Take a look at the following link - W3Schools - Python String Methods and try some of these out. You will need some of them for the TASK.


=== TASK ===

Copy the following into a new Python file.

sentence = input("Please enter a sentence:\n")

print(sentence)

Amend the code so that the inputted sentence is then printed out as

  1. upper case
  2. lower case
  3. first character of each word is upper case

For example,

Please enter a sentence:
The quick brown fox jumps over the lazy dog
THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
the quick brown fox jumps over the lazy dog
The Quick Brown Fox Jumps Over The Lazy Dog

References

W3Schools - Python String Methods

String Indexing and Slicing

A string is a sequence of characters representing Unicode characters.

For example the string "Helloworld" can be thought of like this.

Hello World String

Unlike many programming languages, Python does not have a character type and a character is just a string (str) of length 1.

1. String Indexing

You can access elements of the string using square brackets [] and the index of the position you wish to access.

Note indexing starts at 0. So the first character has an index of 0.

String indexing

For example,

str1 = "String Indexing"

print(str1[0]) # prints the first character "S"
print(str1[4]) # prints the 5th character "n"

1.1 IndexError

If you try the following:

str1 = "String Indexing"

print(str1[15]) # IndexError

You will get an exception as there is no 16th character (index 15 tries to access the 16th character).

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

1.2 Negative Indexing

We can also access the characters of the string from the end.

String indexing

For example,

str1 = "String Indexing"

print(str1[-1]) # prints the last character "g"
print(str1[-2]) # prints the second to last character "n"

1.3 Length of a String

The len() function will return the length of the string.

str1 = "String Indexing"

print(len("String Indexing")) # prints 15

2. String Slicing

We can easily get substrings from a given string by using string slicing.

For example, we can get "Index" from the string "String Indexing".

The following gets the characters from position 7 to position 11 (12 not included)

str1 = "String Indexing"

print(str1[7:12]) # prints Index

Basically you specify the start index and the end index (not included), separated by a colon, to return a part of the string.

2.1 Start to a Given Position

You can get all characters from the start to a given position by:

str1 = "String Indexing"

print(str1[0:6]) # prints String
## OR
print(str1[:6]) # prints String

The second way is very common and is shorthand for the start index 0.

2.2 A Given Position to the End

You can get all characters from a given position to the end of the string using.

str1 = "String Indexing"

print(str1[4:15]) # prints ng Indexing
## OR
print(str1[4:])   # prints ng Indexing

The second way is quite common and it means we don't need to know the length of the string!

Otherwise, in general you would need to do the following

str1 = "String Indexing"

print(str1[4:len(str1)]) # same as str1[4:15] or str1[4:]

2.3 Negative Slicing

You can also slice using negative indexing.

str1 = "String Indexing"

print(str1[-3:]) # prints out the last 3 characters - "ing"
print(str1[-4:-2]) # prints out character 11 and 12. Same as str1[11:13]

=== TASK===

Copy the following into a new Python file.

# DO NOT EDIT THESE TWO LINES
firstname = input("Please enter a first name: \n")  # leave this alone!
surname = input("Please enter a surname: \n")  # leave this alone!

# -------------------------------------------------------

# Edit the following line so that it prints out the first character of the first name
print(f"The first character of the first name is {firstname}")

# Edit the following line so that it prints out the last character of the surname (negative indexing)
print(f"The last character of the surname is {surname}")

# Edit the following line so that it prints the initials of the person. e.g. Mary Smith would result in M.S
print(f"The person's initials are {firstname}.{surname}")

# Edit the following line so that it prints the first 3 characters of the first name. For example Mary would print out Mar
print(f"The first 3 characters of the first name are {firstname}")

# Edit the following line so that it prints the last 4 characters of the surname
print(f"The last 4 characters of the surname are {surname}")
  1. Edit the following line so that it prints out the first character of the first name
print(f"The first character of the first name is {firstname}")
  1. Edit the following line so that it prints out the last character of the surname (negative indexing)
print(f"The last character of the surname is {surname}")
  1. Edit the following line so that it prints the initials of the person. e.g. Mary Smith would result in M.S
print(f"The person's initials are {firstname}.{surname}")
  1. Edit the following line so that it prints the first 3 characters of the first name. For example, Mary would print out Mar
print(f"The first 3 characters of the first name are {firstname}")
  1. Edit the following line so that it prints the last 4 characters of the surname (note we assume for simplicity that the surname contains at least 4 characters, what happens if it is less than 4?)
print(f"The last 4 characters of the surname are {surname}")

An example of the correct program for the input Mary Smith is given below.

Please enter a first name:
Mary 
Please enter a surname:
Smith
The first character of the first name is M
The last character of the surname is h
The person's initials are M.S
The first 3 characters of the first name are Mar
The last 4 characters of the surname are mith

Unit 3 - Branching and Decisions

So far in this course, we have written straight-line programs. These execute one statement after another.

These aren't particularly interesting programs.

Branching programs provide more complexity to our programs and make them more interesting.

Generally, we will look to use a conditional statement which has three parts:

  1. boolean test - an expression that evaluates to True or False
  2. True block - a block of code that is executed if the test evaluates to True
  3. False block - a block of code that is executed if the test evaluates to False

 

The following image demonstrates the program flow of a basic branching statement.

You can see that the code enters the conditional statement (dotted box) and encounters the Test, based on whether it evaluates to True or False it executes either the True block or the False block.

Flow chart for conditional statement Image reproduced from Chapter 2: Introduction to Computation and Programming Using Python.

If ... Else Statement

As stated in the overview of this unit. A conditional statement has three parts:

  1. Boolean test - an expression that evaluates to True or False
  2. True block - a block of code that is executed if the test evaluates to True
  3. False block - a block of code that is executed if the test evaluates to False

1. Structure of a conditional statement

In Python, a conditional if statement has the following form:

if <boolean expression>:
  # If the boolean expression is True, run this block of code
else:
  # If the boolean expression is False, run this block of code
  

You can see how this mirrors the diagram from the overview of this unit, if the boolean expression is True, execute the True block, else it is False, execute the False block.

Conditional flow diagram

Example 1.1 - Just an if statement

The following example tests if a variable x is less than5 and only prints out if x is less than 5

print("code before if statement")

# store 4 in the variable x
x = 4

# Test the variable using the boolean expression x < 5?
if x < 5:
    print(f"{x} is less than 5")
  
print("code after if statement")

The whole output of the program is as follows:

code before if statement
4 is less than 5
code after if statement

If we change x = 10 then the program will not execute the statement print(f"{x} is less than 5") and the output will be:

code before if statement
code after if statement

Copy and paste the following program into a Python file and experiment by entering different numbers.

# get some input from the user and store as an int
input_string = input("Please enter a whole number\n")
x = int(input_string)

print("code before if statement")

if x < 5:
    print(f"{x} is less than 5")
  
print("code after if statement")

Example 1.2 - if ... else statement

The following example tests if a variable x is less than 5 and prints out information for both cases.

print("code before if statement")

# Test the variable using the boolean expression x < 5?
if x < 5:
    print(f"{x} is less than 5")
else:
    print(f"{x} is greater than or equal to 5")
  
print("code after if statement")

In this example x = 7 which is greater than 5, therefore x < 5 evaluates to False and the program prints 7 is greater than or equal to 5 (the else block)

The whole output of the program is as follows:

code before if statement
7 is greater than or equal to 5
code after if statement

Try copying the code below into a Python file and running the program. Think about the following:

  1. What happens with different int values of x?
  2. What happens if x is not an int, for example of type str?
# get some input from the user and store as an int
input_string = input("Please enter a whole number\n")
x = int(input_string)

print("code before if statement")

# Test using the boolean expression x < 5?
if x < 5:
    # run this code if boolean expression is True
    print(f"{x} is less than 5")
else:
    # run this code if boolean expression is False
    print(f"{x} is greater than or equal to 5")
  
print("code after if statement")

2. Indentation

You will notice that the code is indented, this is how Python determines blocks of code. Indentation is determined by the programmer, I would stick to 2 or 4 spaces. Repl.it defaults to 2 spaces.

It can be done using a different number of spaces or the tab character, but you should be consistent in your program. Try the code without the indentation. What happens?

Example 2.1 - No indentation

print("code before if statement")

x = 4

if x < 5:
print(f"{x} is less than 5")
else:
print(f"{x} is greater than or equal to 5")

print("code after if statement")

You will see that you get the following error - IndentationError: expected an indented block.

3. Compound Boolean Expressions

The conditional test for the if statement can be a more complicated expression as long as it evaluates to True or False.

For example, the following program tests to see if a number is between 1 and 10:

Example 3.1 - Logical And

# get some input from the user and store as an int
x = int(input("Enter a whole number:\n"))

if (x > 0) and (x < 11):
  print("Number is between 1 and 10")
else:
  print("Number is not between 1 and 10")

Here the boolean expression (test) is (x > 0) and (x < 11) which requires that both (x > 0) and (x < 11) need to be True for the whole expression to be True.

Example 3.2 - Logical Not

# ask the user for a number and cast it to an int
x = int(input("Enter a whole number:\n"))

if not x < 10 :
  print("Number is above 10")

Here the boolean expression (test) is not x < 0. This will first evaluate x < 0 and then take a not of the result. e.g. if x = 11 then x < 10 is False, therefore not x < 10 is True.

Try out these programs and make sure you understand what they do.


=== TASK ===

You can test if a number is even or odd using the modulus operator %.

For example, 4 % 2 = 0 evaluates to 0 because 2 divides 4 with no remainder.

However, 7 % 2 = 1 evaluates to 1 because 2 divides 7 with remainder 1.

We can use this to evaluate if a number is odd or even. The expression x % 2 == 0 evaluates to True if x is even and False if it is odd.

The expression x % 2 == 0 compares the left side x % 2 to the right side 0 to see if they are equal.

For example,

xx % 2x % 2 == 0
40True
71False

I suggest you try some even and odd examples out in the terminal if you don't understand this.

E.g. Try:

# test 4 to see if it is even
4 % 2 == 0  # will print out True as 4 % 2 evaluates to 0
# test 7 to see if it is even
7 % 2 == 0  # will print out False as 7 % 2 evaluates to 1

Write a program that asks a user for a number and then prints out whether it is even or odd.

Your program should work as follows:

Please enter a whole number:
7
Your number is odd!
Please enter a whole number:
4
Your number is even!

Note that to pass the tests you must have exactly the output above, apart from the numbers which will differ depending on what the user inputs.


MP: The Really Rubbish Password Program

This is an example mini-program to demonstrate the use of an if ... else statement.

The aim is to ask the user for a password and if the password matches the secret password then the user gets into the system, if not the user is denied access.

Feel free to play around with the program as much as you like.

Planning our program in pseudocode.

Set the secret password

Ask the user to enter their password
if the input matches the secret password
  grant access
else
  do not grant access

1. The Complete Program

The complete program is given below and then explained in the subsequent sections.

# The Really Rubbish Password Program

# This is the stored password for the user
secret_password = "secret"

print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth\n")

password = input("Please enter your password:\n")

if password == "secret":
  print("\nAccess Granted!")
else:
  print("\nAccess Denied!")

input("\n\nPress the enter key to exit.")

2. Breaking Down the Program

The following section explains the two main parts of the program.

2.1 Getting the User Input.

The first part of the code prompts the user for their password and stores it in the variable password.

# The Really Rubbish Password Program

# This is the stored password for the user
secret_password = "secret"

print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth\n")

password = input("Please enter your password:\n")

2.2 Testing the Password

The second part of the code then uses the variable password to check if the password is correct. If the condition password == "secret" is True then access is granted otherwise (else) access is denied.

if password == "secret":
  print("\nAccess Granted!")
else:
  print("\nAccess Denied!")

input("\n\nPress the enter key to exit.")

References

Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.

Nested if Statements

It is possible to nest if statements within other if statements. This can be useful for testing multiple conditions but allowing us to run code for each conditional.

1. Nested if Statement

We can nest if statements as follows:

1.1 Example - Nested if Statement

x = 101

if x > 100:
  print("Above 100, ", end="")
  if x > 150:
    print("and also above 150!")
  else:
    print("but not above 150!")

As x = 101 the first program will first execute the statement print("Above 100, ", end="") and then execute the statement print("but not above 150!"). Try different values of x by pasting the code above into a Python file.

If we compare this with:

x = 101

if (x > 100) and (x > 150):
  print("Above 100, and also above 150!")

The second program cannot do this and only prints out "Above 100, and also above 150! if x is greater than 150.

If x = 99 or x = 130 the program will print nothing.

You should make sure you understand the program flow. The two program flows are depicted in the diagram below.

Nested if statement flow

2. Indentation

You should note that in the example given in 1.1, the blocks of code are given by indentation. If we just examine the structure of the example it looks as follows:

# main block of code, anything aligned with this is in this block
if x > 100:
  # True block of code
  print("Above ten,")
  if x > 150:
    # True block of code
    print("and also above 150!")
  else:
    # False block of code
    print("but not above 150!")

This is also illustrated by the diagram below. You should note that the indentation defines each of the blocks of code.

Nested if statement flow with blocks

=== TASK ===

Using nested if statements, write a program that asks the user for a whole number. Your program should do the following:

  • If the number is divisible by 3 and 5 it should print out

Your number is divisible by 3 and 5.

  • If the number is divisible by 3 and NOT 5 it should print out

Your number is divisible by 3 and NOT by 5.

  • If the number is NOT divisible by and by 5 it should print out

Your number is NOT divisible by 3 and is divisible by 5.

  • If the number is NOT divisible by and by NOT 5 it should print out

Your number is NOT divisible by 3 and 5.

Your program should match the examples below. I have given examples for each output.

Please enter a number:
15
Your number is divisible by 3 and 5.
Please enter a number:
12
Your number is divisible by 3 and NOT by 5.
Please enter a number:
20
Your number is NOT divisible by 3 and is divisible by 5.
Please enter a number:
22
Your number is NOT divisible by 3 and 5.

HINT: We learned how you can test if a number is divisible by 2 in the Lesson: If ... Else Statement earlier in this unit.


Elif Statement

Sometimes we may wish to use an alternative test should our previous test evaluate as False.

elif which is short for else if, lets us do exactly that.

1. Using the Elif Statement

We can test another condition after the first condition as follows:

1.1 Example - if ... elif

Remember from Nested If Statements that we can do the following:

# change these to experiment with the if..elif block
x = 4
y = 5

if x < y:
  # block of code
  print("x is less than y")
elif x > y:
  # block of code
  print("x is greater than y")

Here depending on the values of x and y one (and only one) of the print statements is executed.

If x is less than y then x < y is True and the first block is executed.

If x is greater than y then x < y is False and the elif part is checked. As x > y is True, the second block is executed.

What about if x and y are equal?

Copy the code above into a Python file and experiment with different values of x and y.

1.2 Example - if ... elif ..else

We can also include an else statement, now our program will output when x and y are equal.

# change these to experiment with the if..elif..else block
x = 4
y = 5

if x < y:
  # block of code
  print("x is less than y")
elif x > y:
  # block of code
  print("x is greater than y")
else:
  # block of code
  print("x is equal to y")

You should think about this and realise that there are three possibilities.

  1. x is less than y
  2. x is greater than y
  3. x is equal to y

If the first two are False, then it must be that x is equal to y. We don't need a test, we can just the else statement.

1.3 The Connection with if ... else

The program from the last section:

# change these to experiment with the if..elif..else block
x = 4
y = 5

if x < y:
  # block of code
  print("x is less than y")
elif x > y:
  # block of code
  print("x is greater than y")
else:
  # block of code
  print("x is equal to y")

Can be rewritten in terms of just if and else statements:

# change these to experiment with the if..elif..else block
x = 4
y = 5

if x < y:
  # block of code
  print("x is less than y")
else:         # You can see from these two lines where elif gets it's name, it is doing the same as else if!
  if x > y:
    # block of code
    print("x is greater than y")
  else:
    # block of code
    print("x is equal to y")

1.4 Example - Multiple elif statements

Another example is testing to see if the first letter of someone's name begins with a vowel.

input_name = input("Please enter your name:\n")

# convert the name to lowercase
name = input_name.lower()

if name[0] == "a":
  print("The name begins with an a")
elif name[0] == "e":
  print("The name begins with an e")
elif name[0] == "i":
  print("The name begins with an i")
elif name[0] == "o":
  print("The name begins with an o")
elif name[0] == "u":
  print("The name begins with an u")
else:
  print("The name does not begin with a vowel")

I suggest trying each of these programs out in Python.

Note that this is not the most efficient way to do this, we can either use the newer match statement available since Python 3.10 (version). We could also do a similar thing using lists


=== TASK ===

Write a program that outputs whether a number is positive, negative, or zero. Your program should accept numbers of type float.

  • If the number is positive it should output Your number is positive!
  • If the number is negative it should output Your number is negative!
  • If the number is zero it should output Your number is zero!  

For example, your program should output the following given these inputs:

Please enter a number:
2.3
Your number is positive!
Please enter a number:
-3.3
Your number is negative!
Please enter a number:
0
Your number is zero!

The Mood Face Program

This is an example mini-program to demonstrate the use of an elif statement.

The program generates a random number between 1 and 3 and then prints out a given mood face.

Work through the three sections.

1. The Random Mood Program

# Mood Computer
# Demonstrates the elif clause
import random
print("Right now I am feeling...")

mood = random.randint(1, 3)

if mood == 1:
  # happy
  print( \
  """
-----------
|         |
| O     O |
|    <    |
|         |
| .     . |
|  `...`  |
-----------
  """)
elif mood == 2:
  # neutral
  print( \
  """
-----------
|         |
| O     O |
|    <    |
|         |
| ------- |
|         |
-----------
  """)
elif mood == 3:
  # sad
  print( \
  """
-----------
|         |
| O     O |
|    <    |
|         |
|   .'.   |
|  '   '  |
-----------
  """)

input("\n\nPress the enter key to exit.")

You'll note that because the multiline strings do not have indentation, the lines,

  print( \
  """
-----------
|         |
| O     O |
|    <    |
|         |
| .     . |
|  `...`  |
-----------
  """)

are actually really one line of code, but the multiline string is split across the 10 lines.

2. Refactoring the Program

If you look carefully you will notice that all the faces have the same first 5 lines. and the same last line.

-----------
|         |
| O     O |
|    <    |
|         |
-----------

Therefore we can just print this out once and then print out the bottom 3 lines depending on the mood.

# Mood Computer
# Demonstrates the elif clause
import random
print("Right now I am feeling...")

# print the first five lines of the face
print("""-----------
|         |
| O     O |
|    <    |
|         |""")

mood = random.randint(1, 3)

# use elif to print mood lines of the face
if mood == 1:
  # happy
  print("| .     . |")
  print("|  `...`  |")
elif mood == 2:
  # neutral
  print("| ------- |")
  print("|         |")
elif mood == 3:
  # sad
  print("|   .'.   |")
  print("|  '   '  |")

# print the bottom line of the face
print("-----------")
input("\n\nPress the enter key to exit.")

This is exactly the same program but slightly shorter.

3. Accept User Input

We make one final change to the program to ask the user for a number between 1 and 3 instead of randomly generating the number.

# Mood Computer
# Demonstrates the elif clause
mood = int(input("Please enter a number between 1 and 3:\n"))

# use elif to print mood lines of the face
if (mood < 1) or (mood > 3):
  print("Illegal mood value!")
else:
  print("Right now I am feeling...\n")

  # print the first five lines of the face
  print("""-----------
|         |
| O     O |
|    <    |
|         |""")
  
  if mood == 1:
    # happy
    print("| .     . |")
    print("|  `...`  |")
  elif mood == 2:
    # neutral
    print("| ------- |")
    print("|         |")
  elif mood == 3:
    # sad
    print("|   .'.   |")
    print("|  '   '  |")

  # print the bottom line of the face
  print("-----------")

input("\n\nPress the enter key to exit.")

Note now if you enter a number that isn't 1,2 or 3 you will get back,

Illegal mood value!

References

Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.

Booleans

This lesson introduces you to the built-in data type bool. Booleans are essential in computer science and take on either a value of True or False.

True and False are keywords in Python and are used to represent their respective boolean values. You can assign them to variables, for example:

my_bool = True
type(my_bool)

The above code assigns the value True to my_bool and then checks the type. Try this in the terminal and you will see that my_bool is now an object of type <class 'bool'>.

1. Boolean Expressions

You can compare two objects of the same type using the comparison operators listed in the table below. These expressions will return either True or False and are known as boolean expressions.

1.1 Comparison Operators

OperatorNameExample
==Equalx == y
!=Not equalx != y
>Greater thanx > y
<Less thanx < y
>=Greater than or equal tox >= y
<=Less than or equal tox <= y

1.2 Comparing Numbers

For example, the following expressions compare int objects and evaluate to:

ExpressionResult
3 < 5True
3 > 3False
6 >= 6True
3 == 5False
3 != 5True

1.3 Comparing Strings

Interestingly you can also compare other objects such as str. The following,

"held" < "helm"

evaluates to True and,

"help" < "helm"

evaluates to False.

Python knows how to compare the order of strings! It looks at each character in turn and compares their order in the alphabet. Try experimenting. For example, what do the following two expressions return?

"hel" < "helm"
"hello" < "helm"

Try to reason about the answers that you get.

1.4 Comparing Booleans

Believe it or not, you can also compare the order of Booleans.

True < False      # (Evaluates to False)
False < True      # (Evaluates to True)

This is because Python also represents True as the bit value 1 and False as the bit value 0. Now the above should make sense!

1.5 Comparing Different Types

Do you know how to order the str "hello" and the int 10? I don't and neither does Python.

"hello" < 10     

results in the following exception:

TypeError: '<' not supported between instances of 'str' and 'int'

Two objects you can mix are numbers (int and float),

4 < 5.6       # (Evaluates to True)

and numbers and booleans (bool),

4 < True      # (Evaluates to False)

because True is also represented by 1.

2. Logical Operators

Boolean expressions can be combined with logical operators to create larger boolean expressions:

OperatorDescriptionExample
andReturns True if both statements are Truex < 5 and x < 10
orReturns True if one of the statements is Truex < 5 or x < 10
notReverse the result, returns False if the result is Truenot(x < 5 and x < 10)

2.1 Order of Precedence and Left-to-Right

All the logical operators given above have lower precedence than the arithmetic operators and comparison operators.

You will also notice that the order of precedence (high to low) for the logical operators is not, and and then or. This means you evaluate not first, then and, then or.

You also evaluate left-to-right.

OperatorName
()Parentheses
**Exponentiation
*, /, %, //Multiplication, Division, Modulus, Floor Division
+, -Addition, Subtraction
==, !=, <, >, <=, >=Comparison Operators
notLogical NOT
andLogical AND
orLogical OR

2.1 Evaluating a Complicated Boolean Expression

The table gives quite a complicated expression for the not example.

not(x < 5 and x < 10) 

Let's look at this for x = 4,

# This is not code, we are manually evaluating to see how Python works with this expression

not(x < 5 and x < 10)     # (Substitute x = 4)
not(4 < 5 and 4 < 10)     # (Evaluate 4 < 5)
not(True and 4 < 10)      # (Evaluate 4 < 10)
not(True and True)        # (Evaluate True and True)
not(True)                 # (Evaluate not True)
False 

and for x = 6,

# This is not code, we are manually evaluating to see how Python works with this expression

not(x < 5 and x < 10)     # (Substitute x = 4)
not(6 < 5 and 6 < 10)     # (Evaluate 6 < 5)
not(False and 6 < 10)      # (Evaluate False and ....)
not(False)                 # (Evaluate not False)
True 

False and 6 < 10 evaluated to False because and requires both expressions to be True. As the first is False, why bother evaluating the second?

This is known as short-circuit evaluation or McCarthy evaluation. Named after the great John McCarthy.

2.2 Truth Tables

We can consider the result of combining two bool variables p and q. The following are known as truth tables and display the result for different combinations of p and q using and and or.

2.2.1 Truth Table for and

pqp and q
TrueTrueTrue
TrueFalseFalse
FalseTrueFalse
FalseFalseFalse

2.2.2 Truth Table for or

pqp or q
TrueTrueTrue
TrueFalseTrue
FalseTrueTrue
FalseFalseFalse

=== TASK ===

For this program you should fix the single line instructed.

Copy the following code into a new Python file.

# DO NOT TOUCH THESE LINES. THEY ARE USED FOR THE INPUT
is_raining = bool(int(input()))
no_hat = bool(int(input()))
#######################################################

# You should fix this line to by forming an expression using is_raining and no_hat to produce the correct result for takes_umbrella. 

# e.g takes_umbrella = is_raining or no_hat

# Note you only have to fix this line. No if statements etc.. required!

takes_umbrella = True

print(takes_umbrella)

Sam doesn't like getting his hair wet and sometimes wears a hat.

  • On days that it is raining and Sam is not wearing a hat, Sam takes his umbrella.
  • On days that it is raining and Sam is wearing a hat, Sam does not take an umbrella.
  • If it is not raining Sam does not take an umbrella.  

We use two variables is_raining and no_hat to represent whether it is raining and if Sam is wearing a hat.

  • If it is raining is_raining = True
  • If Sam is NOT wearing a hat no_hat = True.  

Using a third variable takes_umbrella determine if Sam should take his umbrella by combining is_raining and no_hat.

For example, on days that it is raining and Sam is not wearing a hat the variables will have the following values:

  • is_raining = True
  • no_hat = True
  • takes_umbrella = True  

is_raining and no_hat have been set up for you. Combine them with logical operators to get the correct value of takes_umbrella.

HINT: You should consider the truth table and fill in the missing entries. This will then give you a hint to what the expression should be.

is_rainingno_hattakes_umbrella
FalseFalse?
FalseTrue?
TrueFalse?
TrueTrueTrue

W3Schools - Python Booleans

W3Schools - Python Comparison Operators

Unit 4 - Iteration (Loops)

In the previous module, we looked at how we could write more complicated problems using conditionals (if statement, etc...).

In addition, we can make our programs repeat particular steps by looping (iteration).

Python has two basic loop statements:

  • while loops
  • for loops

Iterations provide more complexity to our programs and make them more interesting.

Generally an iteration (loop) has two parts:

  1. Boolean test - an expression that evaluates to True or False
  2. Loop body - a block of code that is executed if the test evaluates to True

 

The following image demonstrates the program flow of a basic iteration program.

You can see that the code enters the iteration (dotted box) and encounters the Test, if this evaluates to True it runs the loop body and then repeats the test. If it evaluates to False the program continues.

Flow chart for conditional statement Image reproduced from Chapter 2: Introduction to Computation and Programming Using Python.

While Loops

A while loop lets us execute a series of statements as long as the condition (boolean expression) remains True.

A while loop has the basic structure:

while <condition>:
    # Do something until the condition is False

1. While Loop Using a Counter

A good portion of the while loops that you write will involve a counter variable.

The following example demonstrates a basic while loop that prints out the numbers 1 - 5:

i = 1 # Define a variable i and bind the value 1 to it
while i < 6: 
    print(i)  # print out the value of i
    i += 1    # This increments i by 1. i.e. i = i + 1

print("Program has ended!")

Note the indentation here! The indented block is the loop body (the code that is executed inside the loop).

The above code first defines a counter variable i = 1. It then defines a while loop that tests whether i < 6. If this is True it prints out the value of i and then adds 1 to i.

Note it is traditional to use i for a counter variable.

The image below demonstrates how the code runs:

While loop animation

2. While Loop Using a Boolean Variable

Consider the following program which just keeps asking the user for a number until they enter a 0. Then the program ends.

# loop until the user enters 0

input_string = input("Please enter a number:\n")
num = int(input_string)

while num != 0:
    input_string = input("Please enter a number:\n")
    num = int(input_string)

print("Program has ended!")

We can rewrite this program by using a bool variable (program_over) which keeps track of whether the while loop should continue or stop.

After a number is entered, we use an if statement to test if the number entered was 0, if it was, then we set program_over to True. This will then stop the while loop and the program will end.

# loop until the user enters 0

program_over = False

while not program_over:
    input_string = input("Please enter a number:\n")
    num = int(input_string)
    if num == 0:
         program_over = True

print("Program has ended!")

3. Non-terminating While Loop

Now consider the code if you forget to increment (add to) i.

i = 1 # Define a variable i and bind the value 1 to it
while i < 6: 
    print(i)  # print out the value of i

This loop will repeat forever! i will never change its value from 1 and therefore the condition i < 6 will forever remain True.

  • What will be the output?
  • Try it in Python. To stop (exit) the program in the terminal press Ctrl + c.

4. Nested While Loops

Just like if statements, we can nest loops.

The following program shows an outer and an inner loop. This prints out the multiplication table.

i = 1
while i < 13:
    j = 1
    while j < 13:
        print(f"{i} x {j} = {i*j}")
        j += 1
    i += 1

The outer loop has a counter variable i and the inner loop has a counter variable j. Below is a flow diagram of the program.

While loop multiplication flow

The following is of an animation of a similar program (below), but the conditions are changed to allow us to visualise it quicker.

i = 1
while i < 3:
    j = 1
    while j < 3:
        print(f"{i} x {j} = {i*j}")
        j += 1
    i += 1

While loop nested animation


=== TASK ===

Use a while loop to print out the even numbers starting at 2 and up to and including 100.

2
4
6
.
.
.
100

Note that the dots represent numbers between 6 and 100. It saves us writing it all out!

HINT: You only need a single while loop.


The Number Guessing Game

This is an example mini-program to demonstrate the use of while loops and if .. else statements.

The basic aim is a simple game to ask the user to guess a number between 1 and 10.

Feel free to play around with the program as much as you like.

Note: You will need to set the secret_guess to 4.

1. Number Guessing Game

Planning the program is important. The basic pseudocode for this program is:

pick a random number
while the player hasn’t guessed the number or chosen to exit
    let the player guess
if player guessed correct
    congratulate the player

The program in Python is given below.

# The Number Guessing Game 
import random

# randomly generate a number between 1 and 10
secret_guess = random.randint(1,10)
# to pass the test uncomment this line
# secret_guess = 4

print("Welcome to the Number Guessing Game!")
print("You need to try and guess the number between 1 and 10...")
print("If you wish to exit the game enter 0..")

guess = int(input("Please enter a guess:\n"))

while guess != secret_guess and guess != 0:
  print("That is not correct, please try again.")
  guess = int(input("Please enter a guess:\n"))

if guess != 0:
  print(f"Well done the correct answer is {secret_guess}")

This program generates a random number and then asks the user to input a guess.

The guess is then tested in the while condition and if it is not the correct number and not 0 we repeat the step. If it is the correct number we exit the while.

2. Adding Better Feedback

If you try the program above you will get pretty frustrated by the lack of feedback.

To improve the game we can let the user know if their guess is too low or too high.

This is the subject of exercise 4.5.

The Less Rubbish Password Program

This is an example mini-program to demonstrate the use of an if ... else statement and a while loop.

We previously created a rubbish password program using the following code.

# The Really Rubbish Password Program

# This is the stored password for the user
secret_password = "secret"

print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth\n")

password = input("Please enter your password:\n")

if password == secret_password:
  print("\nAccess Granted!")
else:
  print("\nAccess Denied!")

input("\n\nPress the enter key to exit.")

Aside from this being very insecure, a major issue is that the user only gets one guess at entering their password.

1. Improving with a Loop

To improve our program we can add a while loop that repeats the block of code.

password = input("Please enter your password:\n")

if password == "secret":
  print("\nAccess Granted!")
else:
  print("\nAccess Denied!")

To do this we add a boolean variable named access which will track whether the user has access to the system. We then set this to True within the if statement when access is granted.

1.1. The Complete Program

# The Less Rubbish Password Program

# This is the stored password for the user
secret_password = "secret"
access = False

print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth")

while not access:
  password = input("\nPlease enter your password:\n")

  if password == "secret":
    print("\nAccess Granted!")
    access = True
  else:
    print("\nAccess Denied!")

input("\n\nPress the enter key to exit.")

Ranges and Lists

Before we come onto for loops we must understand ranges and lists.

1. List

A list in python is used to store multiple values in a single variable.

For example:

list1 = [1,2,3]
list2 = ["Citreon", "Ford", "Audi", "Mercedes"]
list3 = ["Citreon", "Ford", 67, True]

You can access an element in a list using indexing. Each member of the list has an index number starting from 0.

For example the code,

list3 = ["Citreon", "Ford", 67, True]
print(list3[0])
print(list3[2])

results in the output:

Citreon
67

Try creating your own list in the and printing some of its elements.

For now, we will not discuss more about lists until later in the course. It suffices to know that they store multiple values and are heterogeneous (a fancy way of saying they can store different types).

2. Ranges

A range allows us to create a sequence of numbers using the range() function.

range(10) # This will create a range of numbers 0,1,2,3,4,5,6,7,8,9
range(3, 10) # This will create a range of numbers 3,4,5,6,7,8,9
range(2, 10, 2) # This will create a range of numbers 2,4,6,8

Ranges can take either:

  • an end value range(end)
  • a start and end value range(start, end)
  • a start, end, and step value range(start, end, step)

In some ways a range behaves like a list, however, to save space, Python doesn't create the list.

If you type the following into the terminal, you will see that the object type of a range is a range!

a = range(10)
print(type(a)) # prints <class 'range'>

So think of it as a convenient way to store a list of numbers without actually storing them!

By default start is 0 and step is 1.

So range(10) is the same as both range(0,10) and range(0,10,1).

2.1 Convert a Range to a List

If you try and print a range you won't get the numbers. Try the following in the terminal.

a = range(10)
print(a)

and you will get the output:

range(0, 10)

You can convert the range to a list using the list() function.

a = list(range(2, 10)) # converts the range to a list [2,3,4,5,6,7,8,9]
print(a)

If you run the following code in the terminal or a Python file you will get the output:

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

The list() function is forcing Python to build the list in memory.

To prove this to ourselves let's check the size of range(10**6). Note 10**6 = 1000000

import sys
a = range(10**6)
print(sys.getsizeof(a))

This returns 48 which is 48 bytes of memory. Try a range of any size and you will always get 48.

Now let's convert it to a list and print the size:

import sys
a = list(range(10**6))
print(sys.getsizeof(a))

This returns a whopping 8000056 (8 million) bytes, which is approximately 166667 times as large!


=== TASK ===

Using the range() and list() functions, print out the odd numbers from 1 up to and including 99.

Your output should look like this:

[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]

HINT: Make sure you have read Section 2.1


For Loops

The other main way of looping Python is by using a for loop.

1. Structure of a For Loop

A for loop in python is structured as follows:

for x in <iterable>:
  ## do something

Both ranges and lists are iterables which means we can loop over them.

1.1 For Loop with a List

A list is a collection of objects. They don't have to be of the same type.

The following 3 examples show a for loop that loops over each item in the list and prints out that item.

Example 1.1.1 - List of numbers

for x in [1,6,-23]:
  print(x)
1
6
-23

The following animation shows how this works.

For loop

Example 1.1.2 - List of strings

The following loops over a list of strings,

for x in ["Citreon", "Ford", "Audi", "Mercedes"]:
  print(x)

and outputs.

Citreon
Ford
Audi
Mercedes

We can also do this by first assigning the list to a variable and then using this in our for loop.

cars = ["citreon", "ford", "audi", "jaguar"]
for car in cars:
 print(car)

Example 1.1.3 - List of different types

The following loops over a list of different types of objects,

for x in ["Citreon", "Ford", 67, True]:
  print(x)

and outputs.

Citreon
Ford
67
True

1.2 For Loop with a Range

It is common to use the range() function to automatically generate a sequence of numbers. Do you really want to type out the entire list?

It is also faster and more efficient because it does not have to build the list in memory.

Example 1.2.1

Here range(10) generates the numbers 0,1,2,3,4,5,6,7,8,9. You can think of it like the list [0,1,2,3,4,5,6,7,8,9].

The program then prints out double of each item in the list.

for i in range(10):
  print(2*i)
0
2
4
6
8
10
12
14
16
18

Example 1.2.2

Here range(3,10) generates the numbers 3,4,5,6,7,8,9. You can think of it like the list [3,4,5,6,7,8,9].

The program then prints out each item in the list.

for i in range(3,10):
  print(i)
3
4
5
6
7
8
9

Example 1.2.3

Here range(3,20,2) generates numbers using a step size of 2 - 3,5,7,9,11,13,15,17,19. You can think of it like the list [3,5,7,9,11,13,15,17,19].

The program then prints out each item in the list.

for i in range(3,20,2):
  print(i)
3
5
7
9
11
13
15
17
19

1.3 For Loop with Underscore

If you are not using the counter variable in the for loop, it is convention to use an _ (underscore).

for _ in range(10):
  print("Hello")

The above will print out Hello 10 times.

2. Nested For Loops

Just as we did with while loops, we can nest for loops. Here we use an outer for loop and an inner for loop to print out the multiplication tables.

for i in range(1,13):
  for j in range(1,13):
    print(f"{i} x {j} = {i*j}")

The animation below demonstrates how this program works, note that instead of printing out the whole table we have limited this to just 1 and 2.

for i in range(1,3):
  for j in range(1,3):
    print(f"{i} x {j} = {i*j}")

For loop multiplication table

If we compare this to the same program using a while loop, you can see that the for loop version is less verbose and easier to read.

i = 1
while i < 13:
  j = 1
  while j < 13:
    print(f"{i} x {j} = {i*j}")
    j += 1
  i += 1

=== TASK ===

You can find the length of a string by using the len() function.

For example:

string1 = "for loop"
len_string = len(string1)
print(len_string)
# note you could do this in one line print(len(string1))

This will output the length of the str "for loop". That is, 8.

Copy the following into a new Python file:

# Do not touch this line - It is just there to set up the string list from the input
# string_list will contain a list of strings that have been entered.
string_list = input("Please enter a list of numbers separated by a comma.\ne.g. Citreon,Ford,Audi,Mercedes\n").split(',')

print(string_list)

This code will accept a list of comma-separated strings. e.g. Citreon,Ford,Audi,Mercedes.

If you try running the program and you enter a list of comma-separated strings they will be printed back to the terminal.

['Citreon','Ford','Audi','Mercedes']

The strings inputted are stored in the list named string_list.

Amend the program so that it prints out the following for each inputted string.

The length of the string {string} is {length of string}.

For example, for the string "Ford" the program would print:

The length of the string Ford is 4.

For the input one,two,three,four,five, the whole program acts as follows:

Please enter a list of numbers separated by a comma.
e.g. Citreon,Ford,Audi,Mercedes
one,two,three,four,five
The length of the string one is 3.
The length of the string two is 3.
The length of the string three is 5.
The length of the string four is 4.
The length of the string five is 4.

For the input Citreon,Ford,Audi,Mercedes, the whole program acts as follows:

Please enter a list of numbers separated by a comma.
e.g. Citreon,Ford,Audi,Mercedes
Citreon,Ford,Audi,Mercedes
The length of the string Citreon is 7.
The length of the string Ford is 4.
The length of the string Audi is 4.
The length of the string Mercedes is 8.

Note that to pass the tests you must have exactly the output above, apart from the input which will differ depending on what the user inputs.


Break and Continue

break and continue are keywords in python that allow you to stop the loop or skip the current iteration.

Many times programmers use break and continue when they don't need to. Generally break and continue can lead to funny flows in your program and bugs. If you can avoid using them then do!

1. Break

The break keyword allows us to exit ("break") out of a loop early. We can do this with both while and for loops.

Example 1.1

The following program will stop short of printing the numbers 0,1,2,3,4,5,6,7,8,9 and only print 0,1,2,3,4.

If you need to convince yourself, copy, paste and run this code in Python.

i = 0
while i < 10:
    if i == 5:
        break
    print(i)
    i += 1

The program tests each value of i and if it is equal to 5 the program exits the loop.

We can write this without the break.

i = 0
while i < 5:
    print(i)
    i += 1

Example 1.2

Here is the same program with a for loop

for i in range(0,10):
    if i == 5:
        break
    print(i)

We can write this without the break.

for i in range(0,5):
    if i < 5:
        print(i)

2. Continue

The continue keyword allows us to skip the current iteration of a loop. We can do this with both while and for loops.

Example 2.1

The following program will not print out the number 5, but all other numbers.

So the output will be the numbers 0,1,2,3,4,6,7,8,9. Notice that 5 is missing.

If you need to convince yourself, copy, paste and run this in Python.

i = -1
while i < 9:
    i += 1
    if i == 5:
        continue
    print(i)

The program tests each value of i and if it is equal to 5 the program exits the loop.

We can write this without the continue.

i = 0
while i < 10:
    if not i == 5:
        print(i)
    i += 1

Example 2.2

Here is the same program with a for loop

for i in range(0,10):
    if i == 5:
        continue
    print(i)

We can write this without continue.

for i in range(0,10):
    if not i == 5:
        print(i)

3. No Do...While

To date in Python, we have seen a while loop that will check a condition and execute a code block until the condition becomes False.

while <condition>
    # Do something 

In many languages it is possible to do the following:

do
  run a block of code
while <condition>

This guarantees that the code block runs at least once as the while is checked after the code block.

One of the major reasons Python does not support such a statement is that it breaks the indentation rule that a code block should be indented. Here it isn't!

3.1. Try Not To Use Break

It is quite common to see the following in Python.

while True:
    # do something
    if <condition>:
        break

The following essentially emulates a do...while loop. Here is an example.

# The Number Guessing Game 
import random

# randomly generate a number between 1 and 10
secret_guess = random.randint(1,10)

print("Welcome to the Number Guessing Game!")
print("You need to try and guess the number between 1 and 10...")

while True:
    guess = int(input("Please enter a guess:\n"))
    
    print("That is not correct, please try again.")
    
    if guess == secret_guess:
        break
    
print(f"Well done the correct answer is {secret_guess}")

However, the use of a break statement here relies on the person reading this to implicitly understand that the programmer is intending to use a do...while.

There are also other reasons we would not want to do this in other languages which are compiled to do with the fetch-decode-execute cycle.

We can easily rewrite this with an additional boolean.

# The Number Guessing Game 
import random

# randomly generate a number between 1 and 10
secret_guess = random.randint(1,10)

print("Welcome to the Number Guessing Game!")
print("You need to try and guess the number between 1 and 10...")

game_over = False

while not game_over:
    guess = int(input("Please enter a guess:\n"))
    
    print("That is not correct, please try again.")
    
    if guess == secret_guess:
        game_over = True
    
print(f"Well done the correct answer is {secret_guess}")

Now, these are almost identical, however, it is now more clear that the while will end when gameover is set to True. This conveys the programmers' intention.


=== TASK ===

Print out the numbers 1 to 100 but leave out the meaning of life 42.

Use a continue statement.


The Sum Numbers Program

This is an example mini-program.

The aim is to ask the user for a series of numbers. If the user enters 0, the program then outputs the sum of the inputted numbers

Feel free to play around with the program as much as you like.

The first program uses a break to exit the while loop. Notice that the while condition is always True. So the break is essential to exit.

# The Sum Numbers Program
print("Welcome to The Sum Number Program")
total = 0

while True:
  input_string = input("Please enter a number. Enter 0 to stop\n")
  if input_string == "0":
    break
  
  x = float(input_string)
  total += x  # note this is shorthand for: total = total + x

print(f"Sum of numbers entered is {total}")

We can also do this without a break by using a bool named program_over. You will notice that this is a little more verbose.

print("Welcome to The Sum Number Program")
program_over = False
total = 0

while not program_over:
  input_string = input("Please enter a number. Enter 0 to stop\n")
  if input_string == "0":
    program_over = True
  else:  
    x = float(input_string)
    total += x

print(f"Sum of numbers entered is {total}")

NOTE: Please avoid using break if you can. It can lead to code that becomes hard to reason about or follow.

While this might seem irrelavant to this program, imagine a much larger program with a more complicated flow and the use of lots of breaks and continues. This can become hard to reason about.

The Check Valid Input Program

The following mini-program demonstrates the use of try..except.

We will look at exceptions in more detail later on; however, it is important to see try and except early on.

1. A Program That Fails

Consider the following program.

num_string = input("Please enter a number:\n")

# try to convert the number using float()
# this will fail if the user doesn't enter anything valid. e.g. hello
x = float(num_string)

print(f"{x} + 5 = {x+5}")

If you enter something which is not a number, Python will throw an exception, in particular, a ValueError.

2. Validating the User Input

We can fix this issue by catching this exception using the try...except statement.

input_valid = False

while not input_valid:
  num_string = input("Please enter a number:\n")
  try:
    num = float(num_string)
    input_valid = True
  except ValueError:
    print("Input is not a valid number.\n")

print(f"{num} + 5 = {num+5}")

Here the program attempts to convert num_string to a float.

If this throws an exception, then we trigger the except block and print out Input is not a valid number.. Note that the line input_valid = True is not executed and thus input_valid remains set to False. Therefore, we ask the user for another number.

This will continue until the user enters input that does not trigger the exception and input_valid is set to True.

Unit 5 - Functions

Functions are such a huge part of programming that we will be spending two weeks (weeks 5 and 7) on this subject.

In this unit we will be discussing:

  • What a function is
  • Why do we use functions
  • Arguments vs Parameters
  • Return Values
  • Variable Scope
  • Passing by Assignment

A function consists of three parts:

  1. The input(s) to the function
  2. The function itself
  3. The output from the function

You can think of a function as a machine (black box) that takes some input(s), does something, and then produces an output.

Once you define a function, you can reuse it. This is the principal reason for their existence. Don't Repeat Yourself! (DRY)

I have included 5 examples of functions. For now, I want you to understand the idea as a concept. We will see examples 1, 2, 4, and 5 in Python very soon!

1. Example Function - Is a Number Odd?

The first function takes in 1 input (an int) and produces 1 output (a bool).

This function is a machine that tells us whether the input is odd.

For now, we don't care how it works, we just assume it does.

The following image depicts the function on the left and then two examples of it being used on the right.

Odd Function Example

2. Example Function - Add Two Numbers

The second function takes two inputs which are numbers (float) and outputs the sum of those two numbers.

Key point, a function can take multiple inputs!

Add Function Example

3. Example Function - What Colour is a Shape?

The third function might look odd as in python we do not have a Shape type (yet!). Later in the course, we will make one.

It serves to demonstrate that a function is just a machine that does something to the input and produces an output.

Colour Function Example

4. Example Function: Print Hello Name

The final two examples are quite common. The first has an input, but no output. It takes in a person's name (e.g "Emily") and prints "Hello Emily" to the terminal.

We will talk about this and its relationship to Python's None.

Colour Function Example

5. Example Function: Print Hello World

The final example has no input and no output. It is just a machine that runs a piece of code.

In this case, it just prints "Hello World".

Colour Function Example

Passing Unit Tests

This is very important

To date, we have been using input/output tests. From now on we will be mainly using unit tests that import functions from your files.

These allow us to test individual functions. We will discuss this properly later on in the module.

There are two main points here:

  1. You should only write code in either a function block. e.g. the add() function.
  2. You should write all other code that runs your program and calls the functions in the if __name__ == "__main__" block.

Any code outside of this can cause problems with the tests!

Function Basics

As we mentioned in the overview a function consists of three parts:

  1. The input(s) to the function
  2. The function itself
  3. The output from the function

You can think of a function as a machine (black box) that takes some input(s), does something, and then produces an output.

Once you define a function, you can reuse it. This is the principal reason for their existence. Don't Repeat Yourself! (DRY)

In essence it is a way of packaging up bits of code for reuse or to make your program more readable.

1. Defining a Function in Python

We define a function using the keyword def, a function name, and the function parameters. We then write the code the function performs in the function body.

def function_name(<formal parameters>):
    <function body>

The easiest way to demonstrate a function is by an example.

Our first function will just add 5 to the input

def add_five(x):
    return x + 5

Note that the return keyword is how you return (output) something from a function. Here the function add_five() returns the result of adding 5 to the input x.

2. Calling a Function in Python

add_five() is now a function that you can call with different values of x. e.g. add_five(10)

Try running the following code,

def add_five(x):
    return x + 5

result = add_five(10)  # call function with 10 and bind result
print(result)      # this prints the value of result

and you will get

15

as the output.

Function Call Visualisation

You can visualise this yourself on Python Tutor.

Note that we could have called this function twice.

def add_five(x):
    return x + 5

result_one = add_five(10)  # call function with 10 and bind result
result_two = add_five(7)   # call function with 7 and bind result
print(result_one)      # this prints the value in result_one
print(result_two)      # this prints the value in result_two

and you will get

15
12

as the output.

2.1 Define a Function Before You Call It

Now try the following,

result = add_five(10)
print(result)

def add_five(x):
    return x + 5

and you will get NameError. This is because the function has not yet been defined, i.e. it is below the function call. Python does not know about that name.

2.2 Order of Precedence

We can also do more interesting things like:

def add_five(x):
    return x + 5

result = add_five(10)*add_five(7) # same as 15*12
print(result) # prints 180

Here Python will evaluate add_five(10) and then evaluate add_five(7), then multiply them together.

This is because other than parentheses (), function calls have higher precedence than everything else in Python.

2.3 Multiple Return Statements

Consider the following function.

def greater_than_five(x):
    if x > 5:
        return True
    else:
        return False

if __name__ = "__main__":
    print(greater_than_five(3)) # prints False
    print(greater_than_five(7)) # prints True

Here we use an if statement to test whether x is greater than 5, if it is the function returns True, otherwise it returns False.

This is a key point, although your function can only return one output, it can have more than one return statement in your function.

Note that we could have done this with one return statement by returning the result of the boolean expression.

def greater_than_five(x):
    return x > 5

if __name__ = "__main__":
    print(greater_than_five(3)) # prints False
    print(greater_than_five(7)) # prints True

Again which one you use will be your preference and which one you are more comfortable with. Both ways are fine. If the second one isn't for you, then use the first one.

3. if __name__ = "__main__":

You will have noticed in the last couple examples used the if __name__ = "__main__": block. This is not needed to call your functions, but as stated in the 5.1 - MP: Passing Unit Tests you should write any code and calls to your functions inside this block.

The reason is that the unit tests import your code and anything outside the if __name__ = "__main__": will get run due to the import. This can mess up the unit tests!


=== TASK ===

Create a function that tests whether a number is odd or even.

Copy the following into a new Python file.

def odd_test(x):
    # delete pass and replace with code to implement the function
    pass

if __name__ == "__main__":
    print(odd_test(3))     # should output True
    print(odd_test(8))     # should output False

Your function odd_test and take in one input. It should return a True or False depending on the number.

For example:

odd_test(3) should return True and odd_test(8) should return False.


More About Functions

HINT: I would copy and past examples into a Python interpreter to make sure you understand them.

This is one of the most important lessons on functions. Please make sure you understand all 3 of the following.

In this lesson we will cover the following:

  • Different types of functions
  • Parameters vs arguments
  • Function composition

All of these are important concepts and will help you understand later material.

1. Types of Function

1.1 Function with Multiple Parameters

We are not limited to just one parameter, we can have as many as we like. The following two functions take two parameters.

The first returns the sum of the two inputs.

def add(x, y):
    return x + y

if __name__ == "__main__":
    result = add(3, 13)
    print(result) # prints 16

The seconds returns the maximum of the two inputs.

def maximum(x, y):
    if x > y:
        return x
    else:
        return y

if __name__ == "__main__":
    result = maximum(3, 5)
    print(result) # prints 5

You will note that this function has two uses of return for the two possible cases.

1.2. Function with No Return Value

Here we define a function with a single parameter name (the input of the function).

def print_hello(name):
    print(f"Hello {name}")

This is interesting as it takes an input name and does something with the input. However, it does not have a return statement.

Be careful though, this function does return something, it returns the None keyword which represents no value.

Run the following code and you will see this for .

def print_hello(name):
    print(f"Hello {name}")

if __name__ == "__main__":
    # call the function and bind the return value to a
    a = print_hello("Jimi")
    print(a)

You will notice that it outputs,

Hello Jimi
None

because a stores the return value of print_hello() which is None.

1.3. Function with No Input and No Return Value

The final function has no input and no return statement. Its sole purpose is to print two lines.

def print_hello_world():
    print("Hello World")
    print("Welcome to another day on planet earth!")

if __name__ == "__main__":
    print_hello_world()
    print_hello_world()

The code above will output,

Hello World
Welcome to another day on planet earth!
Hello World
Welcome to another day on planet earth!

as the function is called twice.

1.4. Some Familiar Functions

You have already met a bunch of Python functions.

FunctionInputReturnsPurposeExample
print()str to output to the terminal.NonePrints the input to the terminal.print("Hello World")
input()str to output to the terminal.str entered by the user.Prints the input to the terminal and returns what the user inputted as a stringinput("Please enter your name:\n")
int()str representation of a whole number, e.g."5"int conversion of input. e.g. 5Converts a str to an int if possible. If not throws ValueErrorint("5")

1.5 The NotImplementedError

Sometimes it is convenient to define a function, but not write the code yet. This is especially useful when planning a program. It is also useful to make sure that if this function is called it doesn't result in a silent bug.

To stop this, we can define a function that raises a NotImplementedError. This as the name suggests means that you have not implemented the function.

Here we set up a program for computing the total and length of a list. The length() function has not been implemented and raises a NotImplementedError.

def total(num_list):
    """ compute the total of the numbers in the list"""
    tot = 0
    for x in num_list:
        tot += x
    return tot

def length(num_list):
    """ count the number of numbers in the list"""
    raise NotImplementedError("The length function has not been implemented yet!")

if __name__ == "__main__":
    num_list = [4,3,5,7,4]
    print(f"The total of the list is {total(num_list)}") # This line will run
    print(f"The number of numbers in the list is {length(num_list)}") # This line will fail

If you try this in a Python file, e.g. one named main.py you will get the output.

The total of the list is 23
Traceback (most recent call last):
  File "main.py", line 14, in <module>
    print(f"The number of numbers in the list is {length(num_list)}") # This line will fail
  File "main.py", line 10, in length
    raise NotImplementedError("The length function has not been implemented yet!")
NotImplementedError: The length function has not been implemented yet!

This is because when we call the length() function it raises the NotImplementedError with the message The length function has not been implemented yet!.

Note thought that Python actually ran the code before it and printed out the list total.

1.6 The Pass Statement

Another way to do this is by using the pass statement. When a programmer doesn't know what to write or is creating a skeleton program they will sometimes put pass. pass will mean the code is valid and can therefore be run.

def total(num_list):
    """ compute the total of the numbers in the list"""
    tot = 0
    for x in num_list:
        tot += x
    return tot

def length(num_list):
    """ count the number of numbers in the list"""
    pass

if __name__ == "__main__":
    num_list = [4,3,5,7,4]
    print(f"The total of the list is {total(num_list)}") # This line will run
    print(f"The number of numbers in the list is {length(num_list)}") # This line will run but prints - The number of numbers in the list is None

The length() function has a pass statement and will now return None.

Try this in a Python Tutor

2. Parameters vs Arguments

It is important to understand the difference between the parameters of a function and arguments. These words are often confused or used interchangeably.

Parameters

Parameters are the names that are in the function definition in between the parentheses ().

Arguments

Arguments are the names that are used in the function call.

The following image shows both the function definition and the function call.

Parameters vs Arguments

3. Function Composition

Function composition is the act of applying a function to the result of another function.

The name is taken from the mathematical concept of function composition.

Consider the following code.

def add5(x):
    return x + 5

def mul2(x):
    return 2*x

if __name__ == "__main__":
    x = 10      # binds 10 to x
    x = add5(x) # binds 15 to x, i.e. 10 + 5 = 15
    x = mul2(x) # binds 30 to x, i.e. 2*15 = 30
    print(x)    # prints 30

We can write this using function composition, note that we evaluate the innermost function first. So work inner to outer.

The example below first evaluates add5() and then evaluates mul2() with the result of add5().

x = 10 # binds 10 to x
x = mul2(add5(x)) # first computes add5(10) = 15, then computes 2*15 = 30 and binds to x
print(x) # prints 30

We can go further and compose three functions. Now we pass the result of the first two to the print() function.

x = 10
print(mul2(add5(x))) # first computes add5(10) = 15, then computes 2*15 = 30 and then prints 30

You could even just do the following.

print(mul2(add5(10))) # does the whole lot in one line!

OK great, we did it in one line!

However, take a look at the readability of the code. The examples which use function composition here are not exactly very readable, in fact, it obfuscates things.

The first example is clear. Assign 10 to x, then add 5 to x, then multiply x by 2 and finally print x.

There are many examples of complex programs that we can write in python with one-liners. This is fine if it is just for your eyes only.

I would suggest for readability you avoid it unless it is necessary or makes sense and is readable.

3.1 Function Composition Isn't Commutative

It is also important to know that function composition isn't commutative, that is just a fancy way of saying that if you change the order of evaluation it doesn't have the same result.

For example,

3 * 5 == 5 * 3 # True, because multiplication doesn't care about the order (commutative)
3 / 5 == 5 / 3 # False, division cares about the order (not commutative)

The following example shows the add5() and mul2() functions composed in the two possible ways give two different results.

def add5(x):
    return x + 5

def mul2(x):
    return 2*x

if __name__ == "__main__":
    x = 10
    y = mul2(add5(x))
    z = add5(mul2(x))
    print(y)  # prints 30
    print(z)  # prints 25

You can try this in Python Tutor and see that y and z are different values because the order of composition was changed.


=== TASK ===

Copy the following into a new Python file. You will need to remove the raise NotImplementedError and replace it with your own code. Make sure you read the whole of task.

def reverse(str_to_reverse):
    raise NotImplementedError("You have not implemented the reverse function")

def get_character(string_one, i):
    raise NotImplementedError("You have not implemented the get_character function")

if __name__ == "__main__":
    print(reverse("This is a string")) # prints "gnirts a si sihT"
    print(reverse("Hello World")) # prints "dlroW olleH"
    print(get_character("This is a string", 3)) # prints "i"
    print(get_character("Hello World", 5)) # prints "o"
    

The first function is called reverse() and has a single parameter, a string. The function should output the reverse of the string.

You should do this using a loop. There is a simple way to do this in Python, but it is good practice to write your own reverse() method.

The second function is called get_character() and has a string and a number as parameters. It should return the ith character of the string.

The table below gives the return values of the functions for different calls.

Function callExpected return value
reverse("This is a string")"gnirts a si sihT"
reverse("Hello World")"dlroW olleH"
get_character("This is a string", 3)"i"
get_character("Hello World", 5)"o"

I suggest you test the above work correctly in the terminal, you should not rely on the tests as the feedback may not be useful.


The Maximum of a List Program

We are now going to recycle our maximum() function.

def maximum(x, y):
    if x > y:
        return x
    else:
        return y

1. Planning the Program

Here is an outline of the program in pseudocode.

Given a list 
Store the first value of the list as current_max
for each element x in the list (not including the first)
    current_max = max(current_max, x)

print out current_max as the result
        

2. Program - Maximum of a list

Let's now implement this and use our maximum() function in a program to loop through a list of numbers and print out the maximum.

def maximum(x, y):
    if x > y:
        return x
    else:
        return y

if __name__ == "__main__":
    num_list = [3,6,2,1,8,4,4,2,7]
    
    # set the current max to the first element in the list
    current_max = num_list[0]
    
    # loop through the list (start at the second element)
    for x in num_list[1:]:
        # store result of current_max and x
        current_max = maximum(current_max, x)
    
    print(f"Maximum number is {current_max}")

Hopefully, you can see that we didn't really care how maximum() worked, we just knew that maximum() takes in an x and y and returns the larger.

3. Program - Maximum of a list (Function)

I now want to take this opportunity to rewrite this. Let's create a function that can take in a list of numbers and outputs the maximum.

def maximum(x, y):
    if x > y:
        return x
    else:
        return y

def max_list(num_list):
    current_max = num_list[0]
    for x in num_list[1:]:
        current_max = maximum(current_max, x)
    return current_max

if __name__ == "__main__":
    list_one = [3,6,2,1,8,4,4,2,7]
    list_two = [30,16,4,45,27,84]
    list_three = [-10,-4,-3,-2]
    
    print(f"Maximum of list 1 is {max_list(list_one)}")
    print(f"Maximum of list 2 is {max_list(list_two)}")
    print(f"Maximum of list 3 is {max_list(list_three)}")

Congratulations, you have just written a function max_list() that takes in a list of numbers and returns the maximum.

This is a reusable function that we don't really care how it works, we just know it works! This is the essence of what we call decomposition and abstraction which we will look at in unit 7.

Interestingly Python provides such a function for us. Try typing this into the Python Interactive Shell.

max([5,2,4,7])

Again let me stress, max() is a built-in function that we do not know how it works. We just know it takes list and returns the maximum.

You can type the following into the Python Interactive Shell to get help information about the max() function.

help(max)

Scope

This can be a tough one to get your head around, so I suggest you speak to the instructors in the labs if it doesn't make sense.

When you define a function you define a new namespace also called a scope.

Any variable defined within the function is said to be a local variable and can only be accessed inside the function (the scope of the function).

We use function parameters and the return value to exchange information between parts of our program.

NOTE: I do not use the if __name__ == "__main__": block here, it is NOT required, but normally it is very good practice to use it.

1. Example 1

Let's illustrate this using the example from Guttag, J.V. (2021).

def f(x):
  # variables defined here are only available to f() - local
  # we also do not have access to variables created outside of f()
  y = 1
  x = x + y
  print(f"x = {x}")
  return x


# main body of code starts here
# note we have left out if __name__ == "__main__" here. It isn't needed, but is recommended.
x = 3
y = 3
z = f(x)
print(f"z = {z}")
print(f"x = {x}")
print(f"y = {y}")

When a function is called, it creates a new stack frame and any variable defined in the function scope exists in this stack frame and is local to this stack frame.

Once the function has finished, the stack frame is removed and the local variables are also removed.

Here x and y are first defined on the global frame. Then we call f(x) which creates a new stack frame and new variables x and y. We say that information is passed to f()

These two x's and ys' are different! I cannot stress the importance of this. The same name, but different scope. They are not the same variable!

I have created an animation from Python Tutor which shows the stack frame that is created when the function is called and the other local variable x.

Scope Animation

I would also copy the example into Python Tutor and follow the program line by line.

2. Example 2

The following code defines two functions f() and g() and calls both of them one after another.

def f(x):
  """ adds 1 to x and prints result"""
  y = 1
  x = x + y
  print(f"x = {x}")
  return x

def g(x):
  """ adds 2 to x and prints result"""
  y = 2
  x = x + y
  print(f"x = {x}")
  return x

# main body of code starts here
x = 3
y = 3
z = f(x)
t = g(x)
print(f"z = {z}")
print(f"t = {t}")
print(f"x = {x}")
print(f"y = {y}")

This has the effect of creating a stack frame for f(). When f() is finished the stack frame is removed.

Next a stack frame for g() is created. When g() is finished the stack frame is removed.

The program now drops back to the global stack frame and completes.

The animation below illustrates this visually.

Scope Animation

Again, I would also copy the example into Python Tutor and follow the program line by line.

3. Example 3

Each call to a function creates a new stack frame.

So if we call a function from within a function, we actually create a new stack frame on top of the previous stack frame.

The reason we call them stack frames is because they get stacked upon each other.

Here f() has been updated to actually call g() from within its function body.

def f(x):
  """ adds g(x) to x and prints result"""
  y = g(x)
  x = x + y
  print(f"x = {x}")
  return x

def g(x):
  """ adds 2 to x and prints result"""
  y = 2
  x = x + y
  print(f"x = {x}")
  return x

# main body of code starts here
x = 3
y = 3
z = f(x)
print(f"z = {z}")
print(f"x = {x}")
print(f"y = {y}")

You will see from the animation or, by experimenting on Python Tutor or using the debugger, that we end up with the following stack frames all at once.

  • Global stack frame
  • f - stack frame for f()
  • g - stack frame for g()

Once g() finishes it is popped off (removed) from the stack and we go back to f(). Once f() finishes we go back to the global frame.

Note that Python Tutor displays them underneath each other, if it helps think of them stacked on top of each other.

Scope Animation


=== TASK ===

Don't skip reading the above lesson, in the long term it won't help you!

This one is a bit of a freebie.

Print out the following about scope, namespaces, local variables, and stack frames

print("When you define a function, you define a new namespace also called a scope.\n")

print("Any variable defined within the function is said to be a local variable and can only be accessed inside the function (the scope of the function).\n")
 
print("Calling a function creates a new stack frame and any variable defined in the function scope exists on this stack frame and is local to this stack frame.")

References

Guttag, J.V. (2021). Introduction to Computation and Programming Using Python, Third Edition (3rd ed.). MIT Press.

Keyword Arguments and Default Values

1. Parameters and Arguments

To date, our functions have just had parameters such as:

def print_name_age(name, age):
    print(f"Hello {name}, your age is {age}")

This function takes two parameters; name and age. When we call the function we can provide arguments (values) to the function that are bound to the parameters.

So print_name_age("Jimi", 27) will bind name = Jimi and age = 27. These are then used in the print statement.

Formally these are known as positional parameters because you call the function and pass the arguments in the correct position.

2. Keyword Arguments and Default Values

Python provides another way of defining the parameters of a function called keyword arguments.

You can think of these as optional arguments, you will though need to give them a default value.

Let's look at an example from Guttag, J.V. (2021).

def print_name(first_name, last_name, reverse = False):
    if reverse:
        print(f"{last_name}, {first_name}")
    else:
        print(f"{first_name}, {last_name}")

print_name() is now a function whereby the third parameter is a keyword argument with a default value of False.

This means we can leave out the keyword argument. We can just call using the two positional parameters first_name and last_name. Inside the function reverse will take on the default value of False.

print_name("Jimi", "Hendrix")  # prints out Jimi, Hendrix

We can also choose to call the function and overwrite the value of the keyword argument, for example we can tell the function to set reverse to False.

print_name("Jimi", "Hendrix", reverse = True)  # prints out Hendrix, Jimi

which prints out Hendrix, Jimi because reverse is True.

There is also nothing stopping you setting reverse to False, this is perfectly legal, but not necessary.

print_name("Jimi", "Hendrix", reverse = False)  # prints out Jimi, Hendrix

2.1 Order matters.

You can't put keyword arguments before positional parameters.

The following is illegal in Python:

def print_name(reverse = False, first_name, last_name):
    if reverse:
        print(f"{last_name}, {first_name}")
    else:
        print(f"{first_name}, {last_name}")

2.2 Another Simple Example

Let's say you want a price() function that calculates discounted price.

This has an obvious default, that is, no discount.

Here is a function that reflects this. Note that discount is a percentage between 0 and 100.

def calculate_price(price, discount=0):
    """ returns new price with discount applied"""
    return price * (1 - discount/100)

if __name__ == "__main__":
    print(calculate_price(30)) # will just return 30. No discount 
    print(calculate_price(30, discount=50)) # will just return 15. 50% discount on the price

We could extend this to add an optional tax argument.

def calculate_price(price, discount=0, tax=20):
    """ returns new price with discount applied and tax"""
    total_before_tax = price * (1 - discount/100)
    total_after_tax = total_before_tax * (1 + tax/100)
    return total_after_tax

if __name__ == "__main__":
    print(calculate_price(30)) # will just return 36. No discount, tax=20
    print(calculate_price(30, discount=50)) # will return 18. 50% discount on the price, tax=20
    print(calculate_price(30, discount=50, tax=25)) # will return 18.75. 50% discount on the price, tax=25

3. A Few More Keyword Arguments

Just to complete the picture. Here is a function with two keyword arguments. You can have any number of keyword arguments.

def print_person(first_name, last_name, reverse=True, age=None):
    if reverse:
        print(f"{last_name}, {first_name}")
    else:
        print(f"{first_name}, {last_name}")
    
    if age:
        print(f"Aged {age}.")

We can call this function a number of different ways.

print("Jimi", "Hendrix") # Prints Jimi, Hendrix
print("Jimi", "Hendrix", age = 27) 
# Prints 
# Jimi, Hendrix
# Aged 27.
print("Jimi", "Hendrix", age = 27, reverse = True) 
# Prints 
# Hendrix, Jimi
# Aged 27.

Some of you may have spotted in the last example that age and reverse are given in a different order to the function definition.

This is deliberate, once you have given the positional arguments, you can give the keyword arguments in any order.

4. Mutable Keyword Arguments (DO NOT USE)

Consider the following code:

def test_keyword_mutability(list_one=[]):
    list_one.append("sam")
    return list_one

if __name__ == "__main__":
    test_keyword_mutability()
    test_keyword_mutability()

We would expect this to print out:

['sam']
['sam']

Instead it will print out:

['sam']
['sam', 'sam']

This wasn't the intention, this is because the keyword argument list_one=[] is mutable and Python stores it the when the function is defined, not when it is called.

We can correct this by doing the following:

def test_keyword_mutability(list_one=None):
    if list_one is None:
        list_one = []
    list_one.append("sam")
    return list_one

if __name__ == "__main__":
  test_keyword_mutability()
  test_keyword_mutability()
['sam']
['sam']

Much better!

For more information you can see this link:

Python Mutable Defaults Are The Source of All Evil

Common Gotchas


=== TASK ===

Write a function called print_list that prints the multiple of each item in the list

For example print_list([4,1,6,7], multiple = 2)

8
2
12
14

For example print_list([4,1,6,7], multiple = 3)

12
3
18
21

By default your function when called without the keyword argument multiple, i.e. print_list([4,1,6,7]) should output:

4
1
6
7

Copy the following into a new Python file to get started.

def print_list():
    pass

if __name__ == "__main__":
    print_list([4,1,6,7], multiple = 2)
    print_list([4,1,6,7], multiple = 3)
    print_list([4,1,6,7])

Note: Due to the way that the tests are written for this task, an incorrect solution may result in the tests looking as though they are "hanging". Refresh the page to reattempt the test after considering why your solution may be incorrect.

References

Guttag, J.V. (2021). Introduction to Computation and Programming Using Python, Third Edition (3rd ed.). MIT Press.

Specification and Docstrings

When defining functions it is important to inform the person using your function of three things

  1. What does the function do?
  2. What are the parameters of the function and what type should they be?
  3. What does the function return?

Sometimes a function won't take parameters or return anything, so not all of these need documenting.

The standard way of doing these in Python is docstrings. A docstring uses triple quotes """.

1. Creating a Docstring

It is easiest to look at a simple example.

The following function is the simple add function we have seen before:

def add(x, y):
    return x + y

The answers to the 3 questions above for this function are

  1. Adds up two numbers and return the result.
  2. x (int or float), y (int or float). These are the numbers to add.
  3. Returns the sum of x and y.

The following code snippet shows how we add these with a docstring.

def add(x, y):
    """
    Adds up two numbers and returns the result.
    
    Parameters
    ----------
    x: int or float
    First number to add.
    y: int or float
    Second number to add.
    
    Returns
    -------
    int or float
      Sum of x and y.
    """
    return x + y

Our basic template is:

    """
    Function description
    
    Parameters
    ----------
    param_name: type
    Description of parameter
    param_name: type
    Description of parameter
    .
    .
    .
    
    Returns
    -------
    type
      Description of return value
    """

We will be using the NumPy standard in this course that works with the documentation generator library Sphinx. We are not looking at this in detail in this course but is important to know about it.

NumPy Standard

If we are missing either the parameters or return statement then we just leave these out of the docstring.

def print_hello():
    """
    Prints Hello.
    """
    print("Hello")
def print_sum(x, y):
    """
    Prints the sum of the two numbers.
    
    Parameters
    ----------
    x: int or float
    First number to add.
    y: int or float
    Second number to add.
    """
    print(x + y)

And even though you would rarely see a function like the following, for completeness.

def five():
    """
    Returns 5
    
    Returns
    -------
    int
    Always returns 5
    """
    return 5

2. Python's help() Function

Python has many built-in functions. You can find an extensive list below.

Python Built-in Functions

One of these functions is help() which will display the docstring of a function.

Try the following in the terminal:

help(abs)

This gives you the docstring for the built-in function abs().

3. When To Document

Generally, when you are hacking away at your own code, documentation probably will be furthest from your mind. I would still suggest putting in a short description.

Every programmer has written some complicated code and then come back to it at a later date and not had a clue what it does. This then requires time, some hints will help you.

Clearly when working with others and on a shared codebase documenting is important. It is also important if people will be using your code and they would like some hints by using the help() function. If you don't write a docstring, there won't be any help!


=== TASK ===

Copy the following into a new Python file. You won't have to understand this code to pass the task, but you will need to read on.

def add(x, y):
    """
    Adds up two numbers and returns the result.

    Parameters
    ----------
    x: int or float
    First number to add.
    y: int or float
    Second number to add.

    Returns
    -------
    int or float
     Sum of x and y.
    """
    return x + y


# Example from Guttang (2021)
def find_root(x, power, epsilon=0.01):
    """
    Find a y such that y**power = x (within epsilon x).

    e.g. For x = 27 and power = 3 then y = 3 as 3**3 = 27.
    i.e. the cubed root of 27 is 3.

    Parameters
    ----------
    x :int or float
    Number we want the root of.
    power: int
    Root number e.g. square root, power = 2.
    epsilon: int or float, default 0.01 
    Error tolerance for the answer.

    Returns
    -------
    float or None
    """
    # if x is negative and power even. No root exists
    if x < 0 and power % 2 == 0:
        return None
    low = min(-1.0, x)
    high = max(1.0, x)
    ans = (high + low) / 2.0
    # check if the answer is close enough
    while abs(ans**power - x) >= epsilon:
        if ans**power < x:
            low = ans
        else:
            high = ans    
        ans = (high + low) / 2.0
    return ans
   
if __name__ == "__main__":
    # add the line of code here
    pass

The built-in help() function allows us to print out the docstring of a function.

You will also see a more complicated function called find_root.

For now, it doesn't matter how it works, but the docstring does tell us what it does and requires a longer description.

Add a line of code to the bottom that prints out the docstring for find_root by using the help() function. Make sure you don't use any indentation so that the code isn't inside a function.

Think carefully, help() is a function that prints out to the terminal and returns None.


The Receive and Return Program

This is an example program from:

Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.

It demonstrates three variations of functions.

display() receives one parameter message, prints it out, and then does not have a return statement. It will therefore return None.

give_me_five() receives no parameters and it returns the value 5.

ask_yes_no() receives one parameter question and asks the user to answer "y" or "n". It returns the user's response.

1. Complete Program

# Receive and Return
# Demonstrates parameters and return values

def display(message):
    print(message)
  
def give_me_five():
    return 5
  
def ask_yes_no(question):
    """Ask a yes or no question."""
    print(question)
    print("\nPlease enter 'y' or 'n': ")
    response = None
    while response not in ("y", "n"):
        response = input().lower()
    return response

if __name__ == "__main__":
    # main
    display("Here's a message for you.\n")
    number = give_me_five()
    print("Here's what I got from give_me_five():", number)
    answer = ask_yes_no("\nDo you like Python? ")
    print("\nThanks for entering:", answer)
    input("\n\nPress the enter key to exit.")

2. The Function Specifications

# Receive and Return
# Demonstrates parameters and return values

def display(message):
    """Prints out the message.
    
    Parameters
    ----------
    message: str
    Message to display.
    """
    print(message)
  
def give_me_five():
    """Returns the value 5.
    
    Returns
    -------
    int
    Always returns 5
    """
    return 5
  
def ask_yes_no(question):
    """Ask a yes or no question to the user and return the answer "y" or "n".
    
    Parameters
    ----------
    question: str
    The question to ask the user.
    
    Returns
    -------
    str 
    The response of the user. Either "y" or "n".
    """
    print(question)
    print("\nPlease enter 'y' or 'n': ")
    response = None
    while response not in ("y", "n"):
        response = input().lower()
    return response

OK, so our code now looks more verbose, but it is much clearer to someone else what the function does.

Sometimes functions are complicated and giving the person reading it a good specification is important.

How this is done can differ between projects and programmers, but in larger codebases, it is very important as programmers may come and go or hundreds or thousands of programmers may work on a single codebase.

Passing by Assignment

WARNING: This is really fundamental and can cause major bugs and errors if you don't understand it.

I strongly suggest that you paste the examples in Python Tutor and work through them.

If you are struggling to follow this then please speak to the instructors in the practicals.

Python differs from other programming languages in that everything is an object.

Some of these objects are immutable (cannot be changed) and some are mutable (can be changed).

Immutable and mutable types are common in most languages and how these are dealt with differs.

We are specifically talking about what Python does.

1. Immutable vs Mutable Types

1.2 Immutable Type

An immutable type is something that once created cannot be changed. Immutable types you have seen in Python are str, float, int and bool.

Consider the following example which uses int. Here we also use the is keyword to see if x and y are the same object in memory.

x = 1
y = x
print(x is y) # prints True. They are the same object in memory

y += 1
print(x) # prints 1
print(y) # prints 2
print(x is y) # prints False. They are now different objects in memory

The first print(x is y) prints True because x and y are both attached to the same immutable object 1.

The reason the second print(x is y) prints False is because to start with x and y are attached to the immutable object 1; however, when we add 1 to y Python creates a new object and reassigns y to 2.

Now x points to the object 1 in memory and y points to the object 2 in memory. Different objects!

1.2 Mutable Type

Lists are mutable. If we do something similar to the above we get a very different result.

Consider the following example which uses list. Here we also use the is keyword to see if list_one and list_two are the same object in memory.

list_one = [1,2,3]
list_two = list_one
list_two.append(4)

print(list_one) # prints [1,2,3,4]
print(list_two) # prints [1,2,3,4]
print(list_one is list_two) # prints True. They are the same object in memory

When we assign list_two = list_one both names are pointing to the same object [1,2,3] in memory.

When we call the append method, we add 4 to the end of the list [1,2,3]. Both list_one and list_two still point to the same list which is now [1,2,3,4]. Hence print(list_one is list_two) prints True.

We can stop this from happening by using a copy.

Here we assign list_two to a copy of the object [1,2,3] in memory. This now means that list_one points to one object [1,2,3] and list_two points to a separate object [1,2,3].

list_one = [1,2,3]
list_two = list_one.copy() # now we create a copy 
list_two.append(4) 

print(list_one) # prints [1,2,3]
print(list_two) # prints [1,2,3,4]
print(list_one is list_two) # prints False. They are now different objects in memory

The append() method is now only called on the object attached to list_two and therefore the object attached to list_one is not updated. Hence print(list_one is list_two) prints False.

2. Passing by Assignment

When we pass arguments to functions, what we are actually doing is binding objects to the parameters of the function.

2.1 Passing an Immutable Type

n Python, a variable is just a name (label) that can be attached to an object. When we pass an object to a function, the function parameter is attached to the passed object

In the example below x will be attached to whatever is passed into the function add_one(). If you pass in an immutable object, as soon as you try to update the object it will create a new object to reflect the changes.

Remember immutable objects can't be changed.

The following example illustrates this. We pass in a number that is attached to num and then add 1. Because we are trying to change an immutable object, we have to create a new object. However, we don't return anything and reassign, so the original variable num_one is not changed.

def add_one(num):
  num += 1 # add one to the local variable x

if __name__ == "__name__":
    num_one = 1
    print(num_one) # prints 1 
    add_one(num_one)
    print(num_one) # prints 1

We can reflect the changes by returning the result and reassigning it to num_one.

def add_one(num):
  num += 1
  return num # return the new object created by adding 1

if __name__ == "__name__":
    num_one = 1
    print(num_one) # prints 1
    num_one = add_one(num_one) # reassign x1 to the result of add_one()
    print(num_one) # prints 2

2.2 Passing a Mutable Type

When you pass a mutable object to a function and change it you are updating the original object. That is doing something to the object passed to the function doesn't create a new copy, it operates on the same object!

Here is a simple example that adds 4 to the end of a list.

def func_one(items):
  """ append 4 to the list. """
  # append adds to the end of the list
  items.append(4)

if __name__ == "__main__":
    list_one = [1, 2, 3]
    func_one(list_one) # call func_one with list_one
    print(list_one) # prints [1,2,3,4] - this has been updated by the function call

The object attached to variable list_one is first passed to func_one() and the variable items is attached to the object.

The function then adds 4 to the end of the list using the append() method. What happens here is both list_one and items point to the mutable list. When it is updated, both are updated as they point to the same object.

Below are some more examples that illustrate passing a list to a function.

def func_one(items):
    """ append 4 to the list. """
    # append adds to the end of the list
    items.append(4)

def func_two(items):
    """ add the list and [4] """
    # this creates a new object by adding the lists items and [4]
    items = items + [4]

def func_three(items):
    """ add the list and [4] """
    # again this creates a new object by adding the lists items and [4]
    items = items + [4]
    # however this time we return the new object
    return items

if __name__ == "__main__":
    list_one = [1, 2, 3]
    func_one(list_one) # call func_one with list_one
    print(list_one) # prints [1,2,3,4] - this has been updated by the function call
    
    list_one = [1, 2, 3]
    func_two(list_one) # call func_two
    print(list_one) # prints [1,2,3] - this has not been updated by the function call
    
    list_one = [1, 2, 3]
    list_two = func_three(list_one) # call func_three and bind to the variable list_two
    print(list_two) # prints [1,2,3,4] - the new object created by the function call

func_one() operates on the original list, this means you don't need to return anything. You could choose to return the list here, but there is no point.

func_two() adds the original list and the list [4]. This creates a new list but we do not return it and therefore we lose the new list.

func_three() adds the original list and the list [4]. This creates a new list and we return it. This means we can use it outside of our function.

I would experiment with these in Python Tutor to make sure you understand what is going on.

3.3 To Return or Not To Return

Generally, if you are operating on a mutable object and you don't need to keep a copy of the original then there is no need to return anything.

If you need the original list intact then you need to copy the original list before you operate on it and return the modified copy. See the next section.

3.4 Copy and Deep Copy

We will talk about these more in unit 6, but we can stop Python from updating the original list by using a copy of the list.

For now, we will only need the copy() function as we will demonstrate on a list of numbers. Things get more complicated if we operate on a list that contains other mutable data types like a list of lists.

import copy

def func_four(items):
  """ append 4 to a copy of the list. """
  # create a copy of the list
  new_list = copy.copy(items)
  # append to the copy
  new_list.append(4)
  # return the copy
  return new_list
if __name__ == "__main__":
    list_one = [1,2,3]
    list_two = func_four(list_one)
    print(list_one)  # prints [1,2,3]
    print(list_two)  # prints [1,2,3,4]

Note, we could have overwritten the original list by reassigning the return value. e.g.

list_one = [1,2,3]
print(list_one) # prints [1,2,3]
list_one = func_four(list_one)
print(list_one)  # prints [1,2,3,4]

=== TASK ===

Create two functions. The first function update_list_item_one() should take a list and a number and update the first item in the list with the number. It should operate on the original list. It does not require a return statement.

For example,

list_one = [1,2,3,4]
update_list_item_one(list_one, 0)
print(list_one) # prints out [0,2,3,4]

The second function new_list_item_one() should create a copy of the list that is passed into the function (this will be a new object), update the first item in the new list and then return the new list.

For example,

list_one = [1,2,3,4]
new_list_item_one(list_one, 0)
print(list_one) # prints out [1,2,3,4]

does not update the original list, however,

list_one = [1,2,3,4]
list_one = new_list_item_one(list_one, 0) # bind the new object to list_one
print(list_one) # prints out [0,2,3,4]

reflects the changes made because the new object returned by new_list_item_one() was reassigned to list_one.

To get you started copy the following into a new Python file.

import copy

def update_list_item_one(items, x):
  pass

def new_list_item_one(items, x):
  pass

if __name__ == "__main__":
    list_one = [1,2,3,4]
    update_list_item_one(list_one, 0)
    print(list_one) # should print out [0,2,3,4]
    
    list_one = [1,2,3,4]
    new_list_item_one(list_one, 0)
    print(list_one) # should print out [1,2,3,4]
    
    list_one = [1,2,3,4]
    list_one = new_list_item_one(list_one, 0) # bind the new object to l
    print(list_one) # should print out [0,2,3,4]

References

Python Docs - Passing by Assignment

Python Memory Model