Part 3. Python basics#

This section introduces the fundamental concepts of Python programming, including variables, data types, control structures, functions, and modules. It provides a solid foundation for writing Python code and understanding how to solve problems using programming.

3.1 Variables#

Variables are used to store information to be referenced and manipulated in a program. They provide a way of labeling data with a descriptive name, so our programs can be understood more clearly by the reader and ourselves. You can give any name to a variable, as long as it follows this rules:

  • It can not start with a number

  • It can not contain spaces or special characters (except for the underscore _).

  • It can not be a reserved word in Python (such as if, else, while, for, def, return, etc.)

A variable is used to store information that can be referenced later on.

We can create an unlimited number of variables; we just have to make sure we give them unique names.

# Creating variables
x = 5               # An integer variable
y = 3.14            # A float variable
name = "Alice"     # A string variable
is_student = True   # A boolean variable

3.1.1 Integer and Float Variables#

What is the difference between the following variables?

a=0
print(a)

a=3*5
print(a)

b="Hello, world!"
print(b)

c=5/0.7142857142857143
print(c)
0
15
Hello, world!
7.0
print("Which type of variable are each of them? ")
print("variable a is ", type(a))
print("variable b is ", type(b))
print("variable c is ", type(c))
Which type of variable are each of them? 
variable a is  <class 'int'>
variable b is  <class 'str'>
variable c is  <class 'float'>

It should be noted that unlike other languages we do not need to specify the type (or class) of this variable. That will simply be assigned from the value it is given.

Here a is automatically assigned as an integer because we have set it to 1. But we can also assign its tyoe explicitly as follows:

a=int(1)
b=float(1)

What is now the difference between a and b?

To display a variable’s value we can use the print() function.

print(a)
print(b)

We have created two variables a and b with the same value but they are different data types.

To check what class a variable, you can use the type() function as follows and we find that a is indeed an integer (denoted by int).

print(type(a))
print(type(b))

We can also update the value of a variable in terms of it’s old value. This may seem very strange as an equation but the = sign should be thought of as an assignment rather than an equals sign in the traditional mathematical sense.

a = 1
print(a)
a = a + 1
print(a)
1
2

It should be noted that this action cannot be performed by a ++ operation (as in C/C++ etc.), but a +=, -= etc. operations can be done.

a = 1.5 # a is redefined as a float with value 1.5
print(a)
a += 0.5 # Here 0.5 is added to a
print(a)
a -= 1 # Here 1 is taken away from a
print(a)
1.5
2.0
1.0

In the last cell you might have noted that I mixed floats and integers in a single operation, and that a a float was returned. This can also be done for multiplication *, division /, powers **, and absolute value abs().

# Multiplication
print(2*5) 
print(2.3*5)

# Powers
print(12.0**2) # returns float
print(12**2) # returns integer
print(pow(12.0,2)) # alternative to **

# Division (Float Division)
print(2/5)
print(1.0/5)
print(3/2.1)

# Absolute Value
print(abs(-4))
print(abs(4.6))
10
11.5
144.0
144
144.0
0.4
0.2
1.4285714285714286
4
4.6

/ will always give float division.

If the integer version of these operations is required one can simply make both vaules an integer and use // for division (this will round down to the nearest integer).

print(19/5) 
print(19//5) 
3.8
3

For those who are not familiar with other languages there is a modulo or remainder operation %. This is the remainder of a division operation which at first may not seem particulary useful but is actually a crucial operation in many algorithms. For example 3 divides into 20, 6 times with remainder 2 as shown.

print(20//3) # Integer division
print(20%3) # Remainder or modulo 
6
2

3.1.2 Scientific Notation#

Consider the following variables. What is the difference between them?

print(123450000000000000) # notice that this is an integer...
print(123450000000000000.0) # and this is a float
123450000000000000
1.2345e+17

The float has been converted to scientific notation with e standing in for 10ˆ.

This can be very useful when dealing with very large or small values. For example,

\[ h \approx 6.626 \times 10^{-34} \text{m}^{2} \text{kg/s} \]
\[ m_e \approx 9.109 \times 10^{-31} \text{kg} \]
\[ M_{sun} \approx 1.989 \times 10^{30} \text{kg} \]
h = 6.626e-34 # Plank's Constant in m^2 kg/s
e_mass = 9.109e-31 # electron mass in kg
sun_mass = 1.989e+30 # solar mass in kg

3.1.3 Comparison Operators and Boolean Variables#

It is often important to compare two variables with >, <, == (equal), != (not equal), >= (greater than or equal to), and <= (less than or equal to. Note that checking if two things are equal we use == rather than = which is reserved for assignment. But what would such an operation give as a value? Well, the statement is either true or false. This is exactly what is returned, a Boolean True or False, a new valiable. Let’s see how it works.

a = True
print(a)
print(1 < 2)
print(2 == 2)
print(7.1 != 7.2) # '!=' refers to not equal
print(4 >= 4.3)
print(type(4 >= 4.3))
True
True
True
True
False
<class 'bool'>

There exists the following operations that act on two Boolean values:

  • and: if both are True, it returns True, otherwise it returns False.

  • or: if either is True, it returns True, otherwise it returns False. Another useful Boolean operator is not which simply reverses the proceeding Boolean. An example of each of these is found below.

print((1 > 2) and (3 > 2))
print(True or (3 <= 2))
print(not (1 == 2))
False
True
True

3.1.4 Complex Numbers#

Python also supports complex numbers with the complex unit denoted by j which acts as one one expect. The extraction of the real and imaginary parts is also shown below.

z = 4 + 3j
w = 1 - 16j

print(type(z))
print(z + w)
print(z * w)

print() # just to seperate out our results

print(abs(z)) # the abs() function takes the modulus of the complex number

print()

print(z.real) # returns the real part of z
print(w.imag) # returns the complex part of w
<class 'complex'>
(5-13j)
(52-61j)

5.0

4.0
-16.0

3.1.5 Strings#

Another type of variable is a string which is an ordered set of characters. This is done using quotation marks. Words and phrases can therefore be outputted and added together (concatenated) with +.

word = 'hello'
phrase = 'how are you?'

whole = word + ', ' + phrase

print(type(word))
print(phrase)
print(whole)
<class 'str'>
how are you?
hello, how are you?

We can also ‘multiply’ a string by an integer in the following way

print((word + ' ')*3)
hello hello hello 

A string can include the new line character or '\n' to have the effect of a return.

print('hello,\nhow are you?')
hello,
how are you?

It is often useful to output variables intersperced in our text. A useful method to do this is the following,

state = 'up'
prob = .8152

text = 'the state was found to be in the %s state with probability %.2f' % (state, prob)

print(text)
the state was found to be in the up state with probability 0.82

Here the %s and the %f stand in for later specified strings and floats respectively. The .2 preceding the f refers to the fact that I want the answer rounded to two decimal places.

%d can be used for an integer and %g for a generic number.

We can find the length of a string using len(),

word = 'hello'
len(word)
5

and we can get specific characters by using [] and groups of characters using :.

word = 'hello'

letter = word[0] 
# notice that in python numbering starts at 0, 
# this notation will be discussed further in the next section on data structures

print(letter)
h

h

e

l

l

o

,

t

h

i

s

i

s

a

l

o

n

g

e

r

s

t

r

i

n

g

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

longer_string = 'hello, this is a longer string'

print(longer_string[7:14]) 
# slices off the section between character 7 (including) 
# and 14 (not including) 

print('\n') # just to seperate our outputs a bit

print(longer_string[:14]) 
print(longer_string[7:]) 
# if the first or second argument is neglected 
# the default value will be 0 and the end of 
# the string respectively.

print('\n')

print(longer_string[7:14:2]) # skips every second character, so prints 7, 9, 11, and 13

print(longer_string[::-1]) # prints backwards 
this is


hello, this is
this is a longer string


ti s
gnirts regnol a si siht ,olleh

Finally you can put the words into a list, an object we will discuss in the next section.

print(longer_string.split())
['hello,', 'this', 'is', 'a', 'longer', 'string']

3.1.6 Casting#

We can change between these classes using the int(), float(), complex(), bool(), and str() functions. This acts as expected with the Boolean casting operator changing everything to True except False, 0, 0.0, the empty string '', and None which we will discuss next.

a = 2.1
print(a)
a = int(a)
print(a)
a = float(a)
print(a)
a = complex(a)
print(a)
a = str(a)
print(a)
print(type(a))
a = bool(a)
print(a)
2.1
2
2.0
(2+0j)
(2+0j)
<class 'str'>
True

3.1.7 None Type#

Finally we have the null variable (denoted by None) which you might see used on occasion.

It should be noted what None is not;

  • None is not the same as False.

  • None is not 0.

  • None is not an empty string.

The None variable is useful when there is no available data, allowing you to store something temporarily. It is helpful in functions, dealing with missing data.

print(type(None))
print(None == False)
print(None == 0)
print(None == '')
print(None == None)
<class 'NoneType'>
False
False
False
True

3.1.8 Operator Precedence#

In this section we have discussed variables and different operations (like addition and division) that you can do with them. Often when writing code we require multiple operations to be done. We know from maths that the order of operations is important for example $\( 2 \times 5 + 3 = 13, \)\( but \)\( 2 \times (5 + 3) = 16. \)$ Here we know the order the operations are performed in is determined by the BODMAS, which stands for Brackets, Orders (or powers/roots), Division, Multiplication, Addition, and Subtraction. In Python operations are performed in the following order.

Operator

Description

()

Parentheses

**

Exponentation

+x, -x

positive, negative

*, /, %

multiplication, division, remainder

+, -

adition and subtraction

<, <=, >, >=, !=, ==

comparison operations

not x

Boolean NOT

and

Boolean AND

or

Boolean OR

This list is incomplete, one can find a complete one here. Consider the following examples of these rules in action

print(2 * 5 + 3) 
print(3 + 5 * 2) 
print((3 + 5) * 2) 

print(3 + 5 / 5) 
print(3 * 5 / 5)
print((3 + 5) / 5) 

print(False and not 6+9<10 or True)
13
13
16
4.0
3.0
1.6
True

In this we had

False and not 6+9<10 or True

There are not brackets, exponents, positives/negatives, multiplication, or division so the addition is done first,

False and not 15<10 or True

The comparison operator is done next giving False,

False and not False or True

The Boolean NOT is then applied,

False and True or True

Then the Boolean AND,

False or True

and finally the Boolean OR,

True

3.2 Data Structures and Operations#

3.2.1 Lists#

Lists are a an ordered collection of items of any data type (mixing data types is allowed). A list of elements in Python uses the following notation.

my_list = [item1, item2, item3, ...]
emptylist = []   # this is an empty list
mylist = [1, -2.0, "Three", 4+0j, None]

print(type(emptylist))
print(type(mylist))
print(mylist)
<class 'list'>
<class 'list'>
[1, -2.0, 'Three', (4+0j), None]

You can access elements in a list using indexing, where the first element has an index of 0.

You can also use negative indexing to access elements from the end of the list.

1

-2.0

"Three"

4+0j

None

0

1

2

3

4

# positive indexing
print(mylist[0]) # first element has index 0
print(mylist[2]) # third element has index 2

# negative indexing, starts from the end
print(mylist[-1]) # last element has index -1
print(mylist[-2]) # second to last element has index -2
1
Three
None
(4+0j)

Lists are mutable, meaning their content can be changed after creation.

print(mylist)   # initial full list

mylist[1] = 2.0 # changing the second element
print(mylist)   # modified full list
[1, -2.0, 'Three', (4+0j), None]
[1, 2.0, 'Three', (4+0j), None]

3.2.2 Tuples#

Tuples are immutable lists, they cannot be modified once created. The elements can be accessed in the same way.

mytuple = (4.1, 6.626e-34, -3, 6.6)

print(type(mytuple))
print(mytuple)
print(mytuple[2])
<class 'tuple'>
(4.1, 6.626e-34, -3, 6.6)
-3

We can use a tuple to assign values to multiple variables in a compact way.

print(mytuple[2])

x, y, z, q = mytuple

print("x =", x) # can print muplitple things with spaces between them in this way
print("y =", y)
print("z =", z)
print("q =", q)
-3
x = 4.1
y = 6.626e-34
z = -3
q = 6.6

3.2.3 Sets#

Sets are an unordered collection of unique items. Sets are useful to eliminate repeated elements.

myset = {2.0, 2 ,4 ,4.0 ,5+2j, 'copy', "copy", 4.1, 3.4e-6} 

print(type(myset))
print(myset)
<class 'set'>
{2.0, 4, 4.1, 'copy', (5+2j), 3.4e-06}

Notice that floats and integers are not distinguished. The data type of the element listed first is the one that is kept.

Note

Ordered vs Unordered data structures.

Ordered data structures guarantee that elements are retrieved in the same order they were inserted while unordered data structures do not maintain any specific order. These are Lists (mutable), Tuples (immutable), and Strings (immutable ordered collection of characters).

Unordered data structures do not guarantee any specific order of elements. These are Sets (mutable collection of unique items) and Dictionaries (mutable collection of key-value pairs).

Although dictionaries preserve insertion order starting from Python 3.7, their keys are unordered, meaning you cannot index or slice them like ordered structures.

3.2.4 Dictionaries#

Dictionaries are similar to lists, but each element has a corresponding unique key that is used to map to that specific element.

mydictionary = {"x" : 7.0, "y" : 53.0,"z" : 2.0}

print(type(mydictionary))
print(mydictionary)
<class 'dict'>
{'x': 7.0, 'y': 53.0, 'z': 2.0}

Access a value using it’s key.

print("1st entry has value =", mydictionary["x"]) 
1st entry has value = 7.0

3.2.5 Operations with Data Structures#

Properties#

We can calculate the length (or number of elements in) these data structures using the len() function.

print(len(mylist))
print(len(mytuple))
print(len(myset))
print(len(mydictionary))
5
4
6
3

We can find the maximum or minimum value of a list, tuple, or set of only real numerical values.

print(max(mytuple))
print(min(mytuple))
6.6
-3

We can check if an element is present in the structure, obtaining a boolean data type. For this we use in in the following way.

print(6.6 in mytuple)
print(5 in mydictionary)
True
False

Changing, Adding, and Removing Elements#

Next we will talk about changing, adding and removing items from lists, sets, and dictionaries. We cannot do this to tuples as they cannot be changed once made.

Changing the values of lists and dictionaries can be done in such a way as you might expect.

print(mylist)
mylist[4] = 5.0
print(mylist)
[1, 2.0, 'Three', (4+0j), None]
[1, 2.0, 'Three', (4+0j), 5.0]
print(mydictionary)
mydictionary['z'] = 3.0
print(mydictionary)
mydictionary['z'] = 2.0
print(mydictionary)
{'x': 7.0, 'y': 53.0, 'z': 2.0}
{'x': 7.0, 'y': 53.0, 'z': 3.0}
{'x': 7.0, 'y': 53.0, 'z': 2.0}

To add to and remove from lists we use the .append(<element>) and .remove(<element>) attributes of the list class. We will discuss this structure in more detail in the object oriented programing portion of the course, however for now we can simply follow the syntax below.

print(mylist)
mylist.append(6.0)
print(mylist)
mylist.remove(6.0)
print(mylist)
[1, 2.0, 'Three', (4+0j), 5.0]
[1, 2.0, 'Three', (4+0j), 5.0, 6.0]
[1, 2.0, 'Three', (4+0j), 5.0]

Append will added to the end of the list but we can put our new item anywhere in our list using .insert(<index>,<elemment>). To delete an element del() can be used ; note that this is not an attribute of the list and can also be used on dictionaries.

mylist.insert(2,2.5)
print(mylist)
del(mylist[2])
print(mylist)
[1, 2.0, 2.5, 'Three', (4+0j), 5.0]
[1, 2.0, 'Three', (4+0j), 5.0]

For sets the structure is very similar with .add() and .remove().

print(myset)
myset.add(6)
print(myset)
myset.remove(6)
print(myset)
{2.0, 4, 4.1, 'copy', (5+2j), 3.4e-06}
{2.0, 4, 4.1, 6, 'copy', (5+2j), 3.4e-06}
{2.0, 4, 4.1, 'copy', (5+2j), 3.4e-06}

For dictionaries we can simply make a new key and use pop() to remove. One can also use del() to delete as noted above.

print(mydictionary)
mydictionary['w'] = 9.0
print(mydictionary)
mydictionary.pop('w')
print(mydictionary)
{'x': 7.0, 'y': 53.0, 'z': 2.0}
{'x': 7.0, 'y': 53.0, 'z': 2.0, 'w': 9.0}
{'x': 7.0, 'y': 53.0, 'z': 2.0}

Slicing and Reorganising#

We can slice (or obtain section of) a list or tuple using the following syntax.

sublist = mylist[0:3] # first three elements
print('mylist =', mylist)
print('sublist =', sublist, '\n') # \n makes a new line

subtuple = mytuple[2:4] # last two elements
print('mytuple =', mytuple)
print('subtuple =', subtuple)
mylist = [1, 2.0, 'Three', (4+0j), 5.0]
sublist = [1, 2.0, 'Three'] 

mytuple = (4.1, 6.626e-34, -3, 6.6)
subtuple = (-3, 6.6)

Or we slice incrementaly.

#This operation extracts the 0th element and takes two steps to extract the third and then fifth element.
print(mylist[0::2])
#Simlary, starting at index 1
print(mylist[1::2])
#If an index is omitted from the operation, it will be interpreted as 0, for example:
print(mylist[::2])
[1, 'Three', 5.0]
[2.0, (4+0j)]
[1, 'Three', 5.0]

For reversing and sorting lists we will need a real numerical list.

numlist = [-20, 6.8e10,6.0,19]
print('list =', numlist)
numlist.reverse() # reverses order of elements in the list
print('reversed list =', numlist)
numlist.sort() # places list in asending order
print('sorted list =', numlist)
list = [-20, 68000000000.0, 6.0, 19]
reversed list = [19, 6.0, 68000000000.0, -20]
sorted list = [-20, 6.0, 19, 68000000000.0]

We can also wipe a list clean by using clear().

numlist.clear()
print(numlist)
[]

Combining Structures#

We can concatenate lists, as shown

L = [7.0,8.0,9.0]
M = [1.0,5.0,9.0,7.0]

concat = L + M
print(concat)
[7.0, 8.0, 9.0, 1.0, 5.0, 9.0, 7.0]

Lets define two sets, using these lists, and calulate their union and intersection.

L = set(L) #recasting list as a set with the same elements
M = set(M)

un = L.union(M)#Returns a union set of the two sets
inter = L.intersection(M)#Returns the intersection of the two sets

print(un)
print(inter)
{1.0, 5.0, 7.0, 8.0, 9.0}
{9.0, 7.0}

Can calculate difference between sets.

print(L.difference(M))#Returns elements in L that are not in the M

print(L.symmetric_difference(M))#Returns elements that are only in one of the sets
{8.0}
{1.0, 5.0, 8.0}

Can also do following actions to sets

print(L.issubset(M))#Checks if L is a subset of the M

print(L.isdisjoint(M))#Checks if L is a disjoint of the M

print(L.issuperset(M))#Checks if L is a superset of the M
False
False
False

Copying#

Copying these data structures can be tricky and it requires some care to avoid bugs.

a = [1,2]
b = a

a[1] = 3 

print(a)
print(b)
[1, 3]
[1, 3]

This is cleary not the desired result; it seems a and b are now inextricably linked together by the = assignment. This is because when we refer to a in our code we are refering to the place where the list a is stored in our memory, we have therefore just com. This can be fixed by using a.copy() which will return a copy of the list which is what is desired.

a = [1,2]
b = a.copy()

a[1] = 3 

print(a)
print(b)
[1, 3]
[1, 2]