## Python Basics

### Variables

Let’s start with variables

```
a = 10
b = 15
c = a - b
print(c)
```

### Elementary Arithmetic Operations

Similarly, we can perform some other arithemtic operations too:

```
d = a*5
print(d)
```

`50`

#### Exponent

For power, we can use the ****** operator.

```
kb = 2**10
print(kb)
```

`1024`

#### Example: Wheat and Checkerboard Problem

As the famous legend states that the chess inventor was asked by the king for the prize and he asked one grain of rice in the first chess cell, followed by two in the other and 4 in the 3rd and so on.

It results in a Geometric series, having the form of:

$$S_{64} = 2^{64}-1$$

```
chessCombinations = (2**64)-(1)
print(chessCombinations)
```

`18446744073709551615`

### Advanced Mathematical Features

For advanced mathematical functions, we can take advantage of the ** math** library.

It can simply be imported as:

`import math`

We can use it for a number of features, like:

```
b = math.cos(30)
print(b)
```

`0.15425144988758405`

The answer is not what we were expecting it to be. Actually, every computational software uses SI units, and hence Radians are used. We can easily convert Radians to Degrees using the formula:

$$Degree = frac{Radians times pi}{180}$$

So,

30º = 30/180*pi

```
b = math.cos(30/180*math.pi)
print(b)
```

`0.8660254037844387`

Similarly, we can use it for plenty of other mathematical functions, like:

#### Exponential Function

```
a = math.exp(4)
print(a)
```

`54.598150033144236`

#### Factorial and Permutations/Combinations

Remember from FSc Mathematics (ch 7?). We have built-in functions for them.

```
a = math.factorial(5)
print(a)
```

`120`

**Combination** is defined as:

$$C(n,k) ={C_k}^n = frac{n!}{k! (n-k)!}$$

We can simply calculate it using Python as `math.comb(n,k)`

.

**Permutation** on the other hand is a phenomenon where order matters, and hence defines as:

$$P(n,k) ={P_k}^n = frac{n!}{(n-k)!}$$

**Example: Birthday Problem**

There are 50 people in this class. What’s the probability of any student sharing the same birthday as me?

$$V_{nr} = frac{n!}{(n-k)!} = frac{365!}{(365-50)!}$$

$$V_{t} = n^{k} = 365^{50}$$

$$P(Same Birthday) = 1 – frac{V*{nr}}{V*{t}}$$

Let’s code it.

```
n = 365
k = 25
V_nr = math.factorial(365)/math.factorial(340)
V_t = math.pow(366,25) #We can use both ** or math.pow(x,y)
Prob_A = 1 - (V_nr/V_t)
print(Prob_A)
```

`0.5972141244558467`

Yeah, its pretty counter-intuitive and mind-blowing. Please feel free to read this Birthday Paradox in the spare time.

Anyways, since obviously we just want a probability and not a NASA rocket precision, so we can round it off.

```
Prob_A = round(Prob_A,3)
print(Prob_A*100,"%")
```

`59.699999999999996 %`

Please feel free to check the other mathematical functions by checking its whole documentation.

### Operator Precedence

DMAS rule from primary school would be clear to everyone. Operators in Python are also evaluated using the similar precedence. If two operators have the same precedence, they are evaluated in the **left to right** order.

```
a = 3/4+9-2*12
print(a)
```

`-14.25`

We can always use parantheses to enforce the precedence (other than the default order).

```
a = 3/4+(9-2)*12
print(a)
```

`84.75`

### Printing a message and a variable together.

We could easily output the above `print(a)`

as `print("Answer after parentheses is:" a)`

Let’s see how to do it

`print("Answer after parentheses is: ", a)`

`Answer after parentheses is: 84.75`

We can even combine two or more variables, for example:

`print("Smallest prime is", 2, "and its surprisingly the only even prime as well. It's 10th power is: ", 2**10)`

`Smallest prime is 2 and its surprisingly the only even prime as well. It's 10th power is: 1024`

## Selection Structures

Often, there are scenarios where we have to pick one out of the two (or several) cases.

For that, we can use ** if** condition.

An `if`

condition takes the form:

```
if (condition):
<statements if condition is true>
```

```
if(Prob_A)>0.50:
print("Its more likely to happen than not.")
```

`Its more likely to happen than not.`

Ok, but what about the cases having $p le 0.5$?

We can use ** else** in case we have both default and counter cases.

```
a = 0.3 #Feel free to change its value.
if(a)>0.50:
print("Its more likely to happen.")
else:
print("Its unlikely to happen.")
```

`Its unlikely to happen.`

### Multiple Cases

Do we have the support for multiple options as well? Yeah! We do.

In such a cases, we design the first condition by `if`

, rest by `elif`

and default one by `else`

.

Which means that if we have 6 conditions, then no of ** elif** would be:

$$6-2 = 4$$

`#if(marks>=80){print()}`

```
marks = 45
if(marks>=80):
print("A+ grade")
elif(marks>=70):
print("A grade")
elif(marks>=60):
print("B grade")
elif(marks>=50):
print("C grade")
else:
print("fail")
```

`fail`

```
name = "Ali"
name
```

`'Ali'`

## Identation

Python’s philosophy is centered around the code’s beautification and ease of understandability. As a result, Python uses identations to separate out different blocks of code.

If you look back at the `if()`

etc conditions, then the statement(s) under them are after an identation, marking their respective blocks.

**Caution:** If you are in some block and want to switch to a new statement outside that block, its necessary to *clear* that block by removing the identation in the new line. Most of the errors for Python beginners stem from the misunderstanding and mishandling of the identations. Once you get used to them, they turn out to be a pretty easy/interesting feature of Python.

We will continue observing identations throughout the course.

## Basic Data Structures

So far, we have observed only scalar numbers as variables. But what about the vectors and other structures? In this section, we will cover them.

### Lists

If a class has 20 students and we have to save the names of every student, one way can be to make 20 variables:

```
student1 = "Ali"
student2 = "Aysha"
student3 = "Bilal"
.
.
.
.
student20 = "Zahid"
```

But clearly its a pretty ugly solution as Software Engineering asks us to generalize things as much as possible.

A better solution would be to make a list (restricted to 6 students here for simplicity).

`student = ["Ali", "Aysha", "Bilal", "Maryam", "Usama", "Zahid"]`

Will `print()`

work in the same way here too?

Also, how to access a particular student’s name?

`print(student)`

`['Ali', 'Aysha', 'Bilal', 'Maryam', 'Usama', 'Zahid']`

#### Indexing

So `print()`

shows us the whole list. Great! But, how about I want to see the name of the 4th student only?

```
a = student[4]
print(a)
```

`Usama`

But it has shown the 5th student here?! The reason is: yes you are right! Python also uses a 0-based indexing. So $0$ refers to the first student, and so on. Which means, Aysha will be found at 1st index:

`print(student[2])`

`Bilal`

#### Slicing

We can even get a subset of the array. This is similar to how we access a number of cells together in the MS Excel by providing a range between starting and ending cell.

`<array's name>[a:b]`

Where $a$ and $b$ are starting and ending indexes.

Please note that the ending index is not counted. So, its similar to the range in mathematics denoted as $[a,b)$.

`student[2:4] #2 index and 3, but 4 not included.`

`['Bilal', 'Maryam']`

### Sets

Python is a cool language and having sets alone is good enough reason to merit it the title.

In sets, order doesn’t matter; also we can’t repeat an element. Sets are represented by curly braces.

```
A = {0, 3, 5}
B = {1, 2, 3, 4}
print(A)
print(B)
```

```
{0, 3, 5}
{1, 2, 3, 4}
```

**Indexing with Sets:** Can we use the indexing with the set elements as well?

`A[2]`

```
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-30-d90411c862f7> in <cell line: 1>()
----> 1 A[2]
TypeError: 'set' object is not subscriptable
```

#### Set Operations

When order doesn’t matter, then indexing doesn’t matter either. We can also use basic set operations, like:

`A.union(B)`

For $A cup B$

`A.intersection(B)`

For $A cap B$

```
C = A.union(B)
D = A.intersection(B)
print("Contents of A: ",A)
print("Contents of B: ",B)
print("Contents of C = A ∪ B: ",C)
print("Contents of D = A ∩ B: ",D)
```

```
Contents of A: {0, 3, 5}
Contents of B: {1, 2, 3, 4}
Contents of C = A ∪ B: {0, 1, 2, 3, 4, 5}
Contents of D = A ∩ B: {3}
```

```
E = B.union(A)
E
```

`{0, 1, 2, 3, 4, 5}`

### Tuples

As we saw that sets are unordered (and hence can’t have duplicates either). There can be some scenarios where we need these items in a proper order.

For example, in **Cartesian coordinate system**, we cannot write $(x,y)$ as $(y,x)$ as well. Both are different from each other.

Like Lahore is situated at (31.32º,74.20º). But if we invert the coordinates, it takes us to the middle of nowhere near Svalbard.

So, in these situations, we need to preserve the order. Tuples are defined by ().

```
LahoreCoordinates = (31.32,74.20)
JeddahCoordinates = (21.32, 39.10)
AmfilochiaCoordinates = (39.10, 21.32)
JavaCoordinates = (-7.29,110.00)
CityXYZ=(5.0,5.0)
```

Now, Jeddah and Amfilochia have opposite coordinates and here tuples are useful as they preserve the order.

```
if(JeddahCoordinates==AmfilochiaCoordinates):
print("Tuples have failed. Simply let us down")
else:
print("Tuples work!")
```

`Tuples work!`

Can we use indexes with the tuples?

`JavaCoordinates[0]`

`-7.29`

```
JavaCoordinates[1] = 12.00
print(JavaCoordinates)
```

```
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-36-85e1dd698786> in <cell line: 1>()
----> 1 JavaCoordinates[1] = 12.00
2
3 print(JavaCoordinates)
TypeError: 'tuple' object does not support item assignment
```

Not suprising one bit as this is precisely why we opted for the tuples.

Now the question arises that why do we require Tuples when Lists can do the same for us?

### Mutability

The difference arises mainly in the mutable nature of the two. Lists are mutable, while tuples aren’t.

Let’s show it further with an example:

```
print("Student list before mutation", student)
student[3]= "Kashif"
print("Student list after mutation", student)
```

```
Student list before mutation ['Ali', 'Aysha', 'Bilal', 'Maryam', 'Usama', 'Zahid']
Student list after mutation ['Ali', 'Aysha', 'Bilal', 'Kashif', 'Usama', 'Zahid']
```

Maryam’s seat has been taken by Kashif here. But tuples ensure that once we have declared the whole data structure, it is **read-only (cannot be modified)** onwards.

```
studentsTuple = ('Ali', 'Aysha', 'Bilal', 'Maryam', 'Usama', 'Zahid')
print("Student Tuple before mutation", studentsTuple)
studentsTuple[3]= "Kashif"
print("Student Tuple after mutation", studentsTuple)
```

```
Student Tuple before mutation ('Ali', 'Aysha', 'Bilal', 'Maryam', 'Usama', 'Zahid')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-40-2a265e53d762> in <cell line: 4>()
2
3 print("Student Tuple before mutation", studentsTuple)
----> 4 studentsTuple[3]= "Kashif"
5 print("Student Tuple after mutation", studentsTuple)
TypeError: 'tuple' object does not support item assignment
```

`testList = [12, 3, 4, "Ali"]`

We haven’t seen the lists of numerical data yet.

For example, take the temperature of next 10 hours for Lahore from Weather.com and store it in the list as:

```
tempForecast = [27,28,28,31,31,30,30,30,29,28]
print(tempForecast)
```

`[27, 28, 28, 31, 31, 30, 30, 30, 29, 28]`

Point to ponder:Should we prefer a list or a tuple here?

Now, lets suppose we have to find the average or maximum temperature in next 8 hours.

```
avg = sum(tempForecast[0:7])/7
print(avg)
```

`29.285714285714285`

The list slicing came pretty handy here. Similarly, we can find the maximum temperature as well:

Point to Ponder:The result of next 10 hours temperature for Lahore will be different at different times. Which sort of variable is this?

```
maxTemp = max(tempForecast[0:7])
print(maxTemp)
```

`31`

**Note:** All these data structures are usually collectively known as **Collections**.

## Loops

We have seen how list slicing and functions etc. can help us do the iterative tasks. But these functions are limited. In order to perform these iterative tasks, we can use loops.

We have two types of loops.

– usually preferable when number of iterations are known already.`for`

– this loop will continue until a given condition doesn’t violate.`while`

`for`

loop

It takes the form of:

```
for <i> in <collection>:
<Do something>
```

Here ** i** is the proxy variable for the current (given iteration’s) member of the

**. This**

`collection`

`collection`

variable can be either a list, tuple or a set.Going back to our ** tempForecast** list. We can iterate through it as:

```
for i in tempForecast:
print(i)
```

```
27
28
28
31
31
30
30
30
29
28
```

Note:For someone coming from a VB or C/C++ background, it would be a bit novel/surprising to see the`for`

loop taking this form in which it can iterate over any collection.

But if you are familiar to C# or Java and have used ** foreach()**, then you would be quick to realize that

**Python’s**.

`for`

loop is both classical `for()`

and `foreach()`

combined together as a single packageWe can apply it in a similar way to the string-based collections as well:

```
for s in student:
print(s)
```

```
Ali
Aysha
Bilal
Kashif
Usama
Zahid
```

```
"""
for x in range(10):
print(x)
for x in range(3,10):
print(x)
"""
for y in range(1,100,2):
print(y)
```

```
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
```

`while`

loop

Similarly, `while`

loop is used when we are unaware of the number of times the loop will run (in advance).

It takes the form:

```
while condition:
<Do something>
```

Now this condition is independent to any container, so can be used anywhere.

For example:

```
x = 0
while(x<10):
input(x)
print(x)
```

```
02
0
03
0
012
0
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
<ipython-input-52-556c6700811a> in <cell line: 3>()
2
3 while(x<10):
----> 4 input(x)
5 print(x)
/usr/local/lib/python3.10/dist-packages/ipykernel/kernelbase.py in raw_input(self, prompt)
849 "raw_input was called, but this frontend does not support input requests."
850 )
--> 851 return self._input_request(str(prompt),
852 self._parent_ident,
853 self._parent_header,
/usr/local/lib/python3.10/dist-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
893 except KeyboardInterrupt:
894 # re-raise KeyboardInterrupt, to truncate traceback
--> 895 raise KeyboardInterrupt("Interrupted by user") from None
896 except Exception as e:
897 self.log.warning("Invalid Message:", exc_info=True)
KeyboardInterrupt: Interrupted by user
```

```
s = student[0]
i = 0
while(s!="Kashif"):
print(s)
i=i+1
s = student[i]
```

```
Ali
Aysha
Bilal
```

## Nested Collections

Let’s recall the example above of several cities coordinates.

Let’s suppose I want to find all the cities (from those cities) which lie in the Northern Hemisphere (i.e, above the equator). For doing that, I need the data in a collection, but all those cities’ data is in the form of different tuples and not a single collection.

So, can I combine those variables (which are tuples themselves) in a single collection?

Yes! We can. We can put them in a `list`

, `tuple`

or whatever suits our requirement.

So, in our case we will use a ** list**.

**Point to Ponder:** Should it be list or a tuple?

```
citiesList = [LahoreCoordinates, JeddahCoordinates, AmfilochiaCoordinates, JavaCoordinates]
print(citiesList)
```

Now we can easily loop over it to do the desired processing. For example:

```
for city in citiesList:
if(city[0]>0):
print("The city with coordinates", city,"is in the Northern Hemisphere.")
```

Sounds good. But none of us is a geologist good enough to realize which city is the one represented by (21.32,39.1), etc. So instead, it would have been a better choice to save the city’s name as well.

For it, we luckily have a data structure named **Dictionary**, which saves both the value and the label.

## Dictionary

A dictionary can be defined as pairs of item and its value (combined with ** :**):

`dict = {"item1":value1, "item2",value2}`

Let’s repeat the above example with a dictionary instead.

`citiesDictionary = {"Lahore":LahoreCoordinates, "Jeddah":JeddahCoordinates, "Amfilochia":AmfilochiaCoordinates, "Java":JavaCoordinates}`

Now lets iterate again.

```
for city in citiesDictionary:
if(city[0]>0):
print("The city",city,"is in the Northern Hemisphere.")
```

```
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-55-447dd729c5f1> in <cell line: 1>()
1 for city in citiesDictionary:
----> 2 if(city[0]>0):
3 print("The city",city,"is in the Northern Hemisphere.")
TypeError: '>' not supported between instances of 'str' and 'int'
```

What went wrong? Nothing to panic. Actually, dictionaries have two components: id (usually known as **key**) and value.

Luckily, we can iterate dictionaries by taking both keys and values in each iteration using the ** items()** method as:

```
for key,value in dict.items():
<Do xyz on key and value>
```

Last point. We need to remember that coordinates themselves are a ** tuple** and hence we need to further index them to fetch the respective coordinate (

**latitude**in our case).

Let’s update the code:

```
for city, coordinates in citiesDictionary.items():
if(coordinates[0]>0):
print("The city", city,"with coordinates", coordinates,"is in the Northern Hemisphere.")
```

```
The city Lahore with coordinates (31.32, 74.2) is in the Northern Hemisphere.
The city Jeddah with coordinates (21.32, 39.1) is in the Northern Hemisphere.
The city Amfilochia with coordinates (39.1, 21.32) is in the Northern Hemisphere.
```

Apparently, using a dictionary with both keys and values as strings sounds counter-intuitive (personally I am not a fan of it), but its allowed due to some scenarios where its required. For example,

- We have addresses as values and we can specify the associated person as a key.
- We have DNA sequences of different genes and the respective gene can be the key (just
`ATGCTGATATA`

isn’t going to make any sense or they are identifiable).

`countriesDictionary = {"US":"Washington","Pakistan":"Islamabad","Iran":"Tehran"}`

Now, if we use an index here, it would just mean the particular character(s) in the string and nothing else. Adding it here just for the sake of the example and that’s it.

```
for country, capital in countriesDictionary.items():
print(country[0])
```

```
U
P
I
```

Last question. Can we make a dictionary having both key and value as numbers?

`numbersDictionary = {1:12,2:34,4:21}`

Python has no qualms about it either. But again, it doesn’t make sense in majority of the cases and must be used only when really required. And whenever I think of any such an example, a thought comes to my mind that it would be a better approach to use a string (like name) as its key instead.

**Question:** What about product id and product name and price?

Well we can deal it in a number of ways, but the best approach to me would be using classes – something we will see in the next notebook. Ciao!

## Questions

That’s it for the day. Questions please!

## Further Reading

**Python Documentation**– should always be open in a tab of your browser.

### Recommended Books

**The Art of Computer Programming, Donald Knuth**– Even if you can understand mere 30% of it, its good enough for you.**Let us Python, Yashavant Kanetkar**– An incredible writer. I am indebted to him for his beautiful book on C++ for learning OOP and C++.

## Leave a Reply