# Define helper function
from pytest import approx
def error_message(actual, expected):
    return f'Actual {actual} != Expected {expected}'

Lecture 1 - Simple data types#

Today, we will get some practice in dealing with:

  • primitive data types (int, float, bool, None, string)

  • binding them to a variable using the assignment operator (=)

  • try to perform simple operations on them (+,-,/,*,//,**)

  • printing them using the print function

  • Use numbers in boolean expressions through comparions (==, !=, >, <)

  • play with string indexing (string_variable[<index>])

The hashtag # in the beginning of a line means that the line is commented-out (its color is different as you see). In other words, this line will not be read by the Python interpreter.

Let us start with simple numeric data types (int, float)

# Set the value of 'integer_one' to the integer value one
integer_one = 1 
# This piece of code uses assertions (lecture 7) to check that your newly defined variable has the correct type and value
assert type(integer_one) == int, error_message(type(integer_one).__name__, int)
assert integer_one == 1, error_message(integer_one, 1)
# Write a print statement to print the value of integer_one
print(integer_one)
1
# Set the value of 'float_one' to the float value one
float_one = 1. 
assert type(float_one) == float, error_message(type(float_one).__name__, float)
assert float_one == 1., error_message(float_one, 1.0)

Now let us continue to see the bool data type in play, which can either be True or False

# Set the value of 'bool_true' to be true
bool_true = True 
assert type(bool_true) == bool, error_message(type(bool_true).__name__, bool)
assert bool_true == True, error_message(bool_true, True)
# Set the value of 'none_type' to 'nothing'
none_type = None 
assert none_type is None, error_message(type(none_type).__name__, None)

Operators#

Things become exciting when we start to do operations on the data.

The format of an expression is:

  • The expression has a value and a type.

  • Parentheses can be used to put precedence to a given operation (just as in calculus)

Examples of operators on the standard int and float data types:

  • +, -, *, /, **, %: (addition, subtraction, multiplication, divide, power, modulus)

# Add the missing operator, replace the ## symbol
nine = 5 + 4
assert nine == 9, error_message(nine, 9)
# Add the missing operator, replace the ## symbol
five = 8 - 3
assert five == 5, error_message(five, 5)
# Add the missing operator, replace the ## symbol
four = 2 * 2
assert four == 4, error_message(four, 4)
# Add the missing operator, replace the ## symbol
two = 4 - 2
assert two == 2, error_message(two, 2)

Remember that there are two ways to divide numbers, normal division (/) and integer division (//)

# Correct the operator
three = 10 / 3
assert three == 3, error_message(three, 3)
assert type(three) == int, "expected an int not a float"
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[20], line 1
----> 1 assert three == 3, error_message(three, 3)
      2 assert type(three) == int, "expected an int not a float"

AssertionError: Actual 3.3333333333333335 != Expected 3
# Correect the operator
two_and_a_half = 5 // 2
assert two_and_a_half == 2.5, error_message(two_and_a_half, 2.5)
assert type(two_and_a_half) == float, "expected a float not an in"

Operator precedence#

Remember that expressions are executed (read) from left –> right, with the following operator precerence:

  • power: **

  • Multiplication: *

  • Division: /, //

  • Addition/Subtraction: +/-

Parentheses can be used to explicitly define precedence

# Use parentheses to fix the following equations
zero = 1 + 1 * 0
assert zero == 0, error_message(zero, 0)
# Use parentheses to fix the following equations
two = 2 ** 3 / 1 + 1 * 2
assert two == 2, error_message(two, 2)
# Use parentheses to fix the following equations
nine = 3 ** 1 * 2
assert nine == 9, error_message(nine, 9) 
# Use parentheses to fix the following equations
three = 18 // (2 * 3)
assert three == 3, error_message(three, 3) 

With the flexibility comes some ugly features. Python allows us to mix int and floats in expressions.#

So be careful! What type will the expression be? Different versions of Python will do different things in certain cases.

  • In Python 3, integer division will be evaluated to a float

  • In Python 2, integer division will be evaluated to an integer and therefore floored (rounded down to nearest integer value)

Remember that you can use type to check the data type.

# If necessary, correct the following expression to yield an integer.
seven = 3 + 4.
assert seven == 7
assert type(seven) == int, error_message(type(seven).__name__, int)
# Correct the following operation to yield a float
nine = 3 + 6
assert type(nine) == float, error_message(type(nine).__name__, float)
# Check the value and type of the following divisions
divisor_float = 3.0 / 2.0
divisor_int = 3 / 2
divisor_int_forced = int(3 / 2)

print(divisor_float, type(divisor_float))
print(divisor_int, type(divisor_int))
print(divisor_int_forced, type(divisor_int_forced))

Using variables to do math#

# Complete the right-hand side of the result_five expression to get the correct result, i.e., replace None
number_two = 2
number_three = 3
result_five = number_two + number_three 
assert result_five == 5, error_message(result_five, 5)
# Complete the 'area_of_square' expression, replace None
side_a = 4
area_of_square = side_a ** 2 
assert side_a == 4, error_message(side_a, 4)
assert area_of_square == 16, error_message(area_of_square, 16)
# Complete the 'circumference of a circle' formula given you have the radius and PI, replace None

PI = 3.14159 # upper case variable names are often used for constants (facts that does not change)
radius = 4
circumference = 2 * PI * radius 
assert circumference == approx(25.13272), error_message(circumference, 25.13272)

In this example, you will use the input() function to prompt the user for inputs.#

Suppose you are an accountant and need to compute the remainder available amount for a customer after paying taxes and rent.

Prompt the user for three inputs:

  • Monthly gross salary (SEK) (i.e., before paying taxes)

  • Tax rate (percentage)

  • Monthly rent (SEK)

Then calculate the remaining allowance.

input() takes a string as input that will be printed as prompt text to the user. What you type in response to the prompt (in str format) will be the output.

In other words, if you enter 10, it will be returned as "10", and you will have to convert (or cast) it to data type float using float().

# Accountant example - read in salary, taxrate and rent and calculate the remaining allowance (remainder)

salary = float(input("Provide your monthly gross salary (SEK): "))
taxrate = float(input("Provide your tax rate (percentage): "))
rent = float(input("Provide your monthly rent (SEK): "))

tax = salary * taxrate/100.
net = salary - tax
remainder = net - rent


print("Remaining amount (SEK):", remainder)
assert type(remainder) == float, error_message(type(remainder), float)

A brief note on memory addresses and rebinding variable names#

  • To access the memory address of an object, we can use the function id().

  • We can convert the memory address to hex code using the function hex().

  • The == operator compares the value of two objects. Most of the time, you will use this.

  • The is operator compares whether two variables point to the same memory id.

    • In other words, if is returns True, then == will necessarily return True BUT NOT necessarily the other way around.

This block contains demonstrations but you can and should play around with examples yourself.

PI = 3.14159
radius = 2.2
area = PI * radius ** 2.

radius_id_original = id(radius) 

print('PI address: ', id(PI), hex(id(PI)))
print('radius address: ', radius_id_original, hex(radius_id_original))
print('area', id(area), hex(id(area)))

If we now reassign a value to the radius variable, we will see that it gets a new memory address.

radius = 3.0
radius_id_new = id(radius)

print(hex(radius_id_original), hex(radius_id_new))

if radius_id_original != radius_id_new: 
    print("The original and new addresses are different.")
    print("This is the correct output.")

else:
    print("The original and new addresses are the same.")
    print("This is the incorrect output. Likely outputted because you forgot to update all cells.")
 

As you can see, we have lost the handle to the original value for the radius (2.2) when we did the reassignment.

Considerations around memory addresses and reassignment will become even more important when we come to compound data types, such as lists.

So please keep the id()and hex() function in mind.

# Let us compare radius and radius_new using == and is operators.
radius = 2.2
radius_new = 2.2

compare_radius_radius_new_w_eq = radius == radius_new
compare_radius_radius_new_w_is = radius is radius_new

print("The two variables have the same value, therefore radius == radius_new should be True: ", compare_radius_radius_new_w_eq)
print("The two variables do not have the same memory address, therefore radius is radius_new should be False: ", compare_radius_radius_new_w_is)

Aliasing: assigning one variable to another variable.#

Aliasing: new_variable = old_variable

new_variable will reference old_variable (i.e., point to the memory address of old_variable)

If you now reassign new_variable to a new value, new_variable will get a new memory address and new_variable and old_variable will no longer point to the same address.

radius = 3.0
radius_new = radius

# Below, I use a fancy string formatting to get a nicer output when passed to the print() function (more in lesson 4)
# If you cannot wait, you can have a look at https://docs.python.org/3/tutorial/inputoutput.html 
print(f"radius:     {radius:5.1f}; address: {hex(id(radius))}")
print(f"radius_new: {radius_new:5.1f}; address: {hex(id(radius_new))}")
#Let us now change the value of radius and see what happens to radius_new.
# Here I am again using the more fancy string format
radius = 4.0
print("Print after updating radius. Note that we did not touch radius_new explicitly.")
print(f"radius: {radius}, address: {hex(id(radius))}")
print(f"radius_new: {radius_new}, address: {hex(id(radius_new))}")

Simple boolean operators#

The simple boolean operators (and, or)

# Set the correct boolean values to yield True with the AND operator. Replace None.
bool_a = True
bool_b = True 
bool_a_AND_bool_b = bool_a and bool_b
assert bool_a_AND_bool_b, error_message(bool_a_AND_bool_b, True)
# Set the correct boolean values to yield True with the OR operator. Replace None.
bool_a = False
bool_b = True 

bool_a_OR_bool_b = bool_a or bool_b
assert bool_a_OR_bool_b, error_message(bool_a_OR_bool_b, True)

Comparison and boolean expressions#

We are going to continue with more practice on data types.

Specifically, we will look at:

  • Comparing number expressions as booleans

  • Boolean comparisons

  • Strings and operations on them

# Make sure the expressions are correct such that a and b together are True. Replace None
a = (5 + 3) == 8 
b = (3 + 3) == 6 

both_correct = a and b
assert both_correct, error_message(both_correct, True)
# Given you have these equations
equation_a = (5 + 3) == 8
equation_b = (3 + 1) == 6

# What should the logical operator between them be to yield True? Replace ##
one_correct_is_okay = equation_a and equation_b #$a$and$

assert one_correct_is_okay, error_message(one_correct_is_okay, True)

Boolean comparisons#

Now you will get some practice in the expected output from Boolean comparison.

Boolean comparison operators

  • bool1 and (&) bool2 : are both values True

  • bool1 or (|) bool2 : are one of the values True

  • bool1 ^ bool2 : (exclusive or) are only one of the values True.

# Given this expression
a = True and True

# Replace None with either True or False
assert a == True 
# given this formula
b = True and False

# Replace None with either True or False
assert b == False 
# given this formula
c = False or False

# Replace None with either True or False
assert c == False 
# given this formula
d = False or True

# Replace None with either True or False
assert d == True 
# given this formula
e = False ^ False

# Replace None with either True or False
assert e == False 
# given this formula
f = True ^ True

# Replace None with either True or False
assert f == False 
# given this formula
g = False ^ True

# Replace None with either True or False
assert g == True 
h = False and not True

# Replace None with either True or False
assert h == False 
i = (True or True) and not (True and True)

# Replace None with either True or False
assert i == False 
j = (False or True) and not (False and True)

# Replace None with either True or False
assert j == True 

Strings and operations on strings#

# Create the string value hello world, replace None with the string
hello = "hello world" 
print(hello)
assert hello == "hello world", error_message(hello, "hello world")
assert type(hello) == str, error_message(type(hello).__name__, str)
# Set the two hello and world variables to their respective words, replace None
hello = "hello" 
world = "world" 

# and combine them in the hello world variable to give "hello world" using the appropriate operator - think about the space in the middle, replace None with the correct expression
hello_world = hello + " " + world 

print(hello_world)
assert hello_world == "hello world", error_message(hello, 'hello world')
assert hello != "", f"value of 'hello' was empty"
assert world != "", f"value of 'world' was empty"
# Depending on how funny a joke is, you might laugh for a longer or shorter time
# Fill in the values (replace None) to make the following print 'HaHaHaHaHa' - replace None
laugh = "Ha" 
many_times = 5 

# make it laugh 'many_times' by implementing the correct expression using the above variables and an appropriate operator 
laugh_a_lot = laugh * many_times 

assert laugh_a_lot == "HaHaHaHaHa", error_message(laugh_a_lot, 'HaHaHaHaHa')
assert many_times > 1, f"many_times should be a number bigger than 1, was {many_times}"
print(laugh_a_lot)

Indexing of strings#

As we have discussed in lecture 1, we can index a str object. This is because it is a sequence of characters.

  • This can e.g., be handy, if you want the names of a particular file type such as docx.

  • Remember that Python counts from 0!

  • If you want to access the value of more than one index, you can use [index_start : index_end] where index_start and index_end are integers.

    • Note that this will give you the values starting from index_start and until index_end - 1

More on this in lesson 4 on compound data types.

# Replace None with name[index] using the correct index inserted
name = 'Nanna'
name_of_first_character = name[0] 
assert name_of_first_character == "N", error_message(name_of_first_character, "N")
# Replace None with the correct index/index range to extract the filename without suffix (docx)
filename_w_extension = 'filename.docx'
filename = filename_w_extension[0:8] 
print(filename)
assert filename == "filename", error_message(filename, "filename")

Comparing strings by order#

Python strins are case-sensitive, and as shown in Lecture 1, upper and lower case letters have different comparison values from the ASCII table.

is_mac_better_than_windows = "Mac" > "Windows"
assert is_mac_better_than_windows == True 

What if we compare stringified numbers?

is_hundred_largest = "100" > "2"
assert is_hundred_largest == True 

Lexical ordering#

When using Python’s sorted() function, it sorts strings by their lexical order (left to right):

the arrangement of a set of items in accordance with a recursive algorithm, such as the entries in a dictionary whose order depends on their first letter unless these are the same in which case it is the second which decides, and so on

So comparison of "banana" and "bAnana"

  • b and b is identical

  • A < a which is True

What you see below is a list of strings!

  • we will come back to lists in Lecture 4, but for now, play with this and see the difference in ordering.

print("b" < "b")
print("A" < "a")
print(sorted(["banana", "bAnana"]))
# And since "A" has a lower value then "a", it will order it first 
# you can use the ord() function to get the ascii value of a single character
print("The ascii value of 1 is", ord("1"))
print("The ascii value of a is", ord("a"))
print("The ascii value of B is", ord("B"))
print("The ascii value of d is", ord("d"))

Lexical ordering becomes really strange when dealing with strings of numbers, which you will see next.

This is often an issue if you have files on your computer that are numbered 1, 2, 3…, 10,11 .. 100…

  • As before, pairwise comparison "1" and "10" both start with 1, meaning they are ordered before "2".

# Python sorts strings Lexicographically, meaning each character is compared one by one from left to right
print(sorted(["1", "2", "5", "10", "50", "100", "200", "500", "1000"]))
# One way to deal with this issue is zero-pad the numbers
print(sorted(["0001", "0002", "0005", "0010", "0050", "0100", "0200", "0500", "1000"]))