Python from the word ...Go (Pt2)
Tags: python, programming, beginners, career
Published: 2022-06-10 12:37:07

**_<u>Basics Part2</u>_**

Helloooo there! Welcome back!!
Wait, are you new here? Don't worry, I got you covered. Here, we are breaking the flow. Have you checked "Python from the word ...Go" [Basics Part1?](https://dev.to/gateremark/python-from-the-word-go-4h93) It's an awesome resource to first check out if you are not familiar with Python's variables and data types which comprise a few in-built Python data structures.
They are really gonna come in handy for this section.

> Set? Lets' go!!

---
In the [previous module](https://dev.to/gateremark/python-from-the-word-go-4h93), we learnt about the fundamental Python data types and also covered some of the terms used when talking about code like variables, statements, expressions, functions, methods ...etc.
Most importantly, we covered how to perform actions on the data types (Both functions and methods for each data type). 
Up until now, we were still scratching the surface. Every time we write code, we wrote it line by line and hence our interpreter would go line by line running each code up to the last line.

```
>>> #do something
>>> #do something
>>> #do something
>>> #do something
```
We are now going to incorporate the idea of running multiple lines over and over to discover the true power of programming for machines, haha!
Hence, in this section, we gonna talk about the idea of **conditions** and **conditional logic**. We gonna discuss more on looping and loops where we can perform actions multiple times over and over.
We are now going to break into a new world where instead of going from line by line in order, we gonna loop over till a condition is met.

---

- **Conditional Logic**

We [previously](https://dev.to/gateremark/python-from-the-word-go-4h93) covered Boolean variables (True or False). When we come to conditional logic, Booleans are super important.
_Example_:
A person(a) wants to purchase a car from a company with specific conditions:

1. The car must be new.
2. The car must have a license.

Hence for person(a) to purchase the car:

- is_new = True

- is_licensed = True

In conditional logic, we use the 'if' keyword.
`"If the car is new and licensed, then person(a) can purchase it".`
Then, if any of the conditions in purchasing the car is not met, person(a) cannot purchase the car.

_Example2_:
Let's assume that for one to get into a specific event, the person has to be old(35 years and above). Create a program to only allow old people to the event.
`If is_old = True, "allowed to get into the event."`
For the syntax:

```
>>> is_old = True
>>> if is_old:
>>>   print("You are allowed to get in")
You are allowed to get in

>>> is_old = False
>>> if is_old:
>>>   print("You are allowed to get in")
#nothing will be printed out.
```
`Note:` Any code that comes after the colon in the condition is automatically indented hence run if the condition is True whereas any code that ain't indented after the condition is not under the condition and hence run separately.
Example:

```
>>> is_old = True
>>> if is_old:
>>>   print("You are allowed to get in")
>>> print("Hello there")
You are allowed to get in
Hello there

>>> is_old = False
>>> if is_old:
>>>   print("You are allowed to get in")
>>> print("Hello there")
Hello there
```

> What if when a condition is not met(False), we want to perform another condition?

Here is an example:
I leave my house;
If it's cloudy, I bring an umbrella;
otherwise, I bring sunglasses.

- We use the keyword **'else'**.

Using our example of letting people in for the event, we can add:
`If is_old = True, "allowed to get into the event.", otherwise if is_old = False, "not allowed to get in",`

```
>>> is_old = True
>>> if is_old:
>>>   print("You are allowed to get in")
>>> else:
>>>   print("Not allowed in")
You are allowed to get in

>>> is_old = False
>>> if is_old:
>>>   print("You are allowed to get in")
>>> else:
>>>   print("Not allowed in")
Not allowed in
```

> `What if by chance you have more than one condition?`

Example:
I'm in a restaurant;
If I want meat, I order steak;
otherwise, If I want Pasta, I order spaghetti and meatballs;
otherwise, I order salad.

For such cases, we use the 'elif' keyword (else if).
Using a different example.

- A person(b) wants to order chicken pizza. If there is no chicken pizza, the person(b) can order beef pizza; otherwise, if there is none, the person(b) can order rice.

```
>>> chicken_pizza = True
>>> beef_pizza = True

>>> if chicken_pizza:
>>>   print("Serve me chicken pizza.")
>>> elif beef_pizza:
>>>   print("Serve me beef pizza.")
>>> else:
>>>   print("Serve me rice.")
Serve me chicken pizza.

--------------------------------------------------
>>> chicken_pizza = False
>>> beef_pizza = True

>>> if chicken_pizza:
>>>   print("Serve me chicken pizza.")
>>> elif beef_pizza:
>>>   print("Serve me beef pizza.")
>>> else:
>>>   print("Serve me rice.")
Serve me beef pizza.

--------------------------------------------------
>>> chicken_pizza = False
>>> beef_pizza = False

>>> if chicken_pizza:
>>>   print("Serve me chicken pizza.")
>>> elif beef_pizza:
>>>   print("Serve me beef pizza.")
>>> else:
>>>   print("Serve me rice.")
Serve me rice.
```
"For a person to be legalized to drive a car in public, one must have a national identification card and a driving license."
These are two conditions that one must have to avoid being fined by the cops.
To develop such a program, we must have both conditions True hence we can use the keyword 'and' to check whether both conditions are True.
When using 'and' both conditions must be True to execute the condition, if any of them is False, the program will run the 'elif' and 'else' part.

```
>>> has_id = True
>>> has_license = True

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Allowed to drive

--------------------------------------------------
>>> has_id = False
>>> has_license = True

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Not allowed to drive

--------------------------------------------------
>>> has_id = True
>>> has_license = False

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Not allowed to drive
```

- Python Indentation

The interpreter in python finds meaning in the spacing hence indentation(tabs) and white spaces in python is essential.

- Truthy Vs Falsy

In python, whenever there's a value, the interpreter recognizes that as True. Otherwise, when there's a zero(0) or no value, python interpreter recognizes that as False.
These are referred to as Truthy and Falsy values.

```
>>> print(bool('hello'))
>>> print(bool(5))
>>> print(bool(''))
>>> print(bool(0))
>>> print(bool(None))
True
True
False
False
False
```
All values are considered "truthy" except the following; which are considered "falsy":

- `None`

- `False`

- `0`

- `0.0`

- `0j`

- `Decimal(0)`

- `Fraction(0, 1)`

- `[]` - an empty `list`

- `{}` - an empty `dict`

- `()` - an empty `tuple`

- `''` - an empty `str`

- `b''` - an empty `bytes`

- `set()` - an empty `set`

- `range(0)` - an empty `range`

- Objects for which:

   - `obj.__bool__()` returns `False`

   - `obj.__len__()` returns `0`

`Note:` A "truthy" value will satisfy the check performed by `if` or `while` statements. We use "truthy" and "falsy" to differentiate from the `bool` values `True` and `False`.
Example:

```
>>> has_id = 'hello'
>>> has_license = 5

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Allowed to drive

--------------------------------------------------
>>> has_id = 'hello'
>>> has_license = 0

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Not allowed to drive

--------------------------------------------------
>>> has_id = None
>>> has_license = True

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Not allowed to drive

--------------------------------------------------
>>> has_id = True
>>> has_license = ''

>>> if has_id and has_license:
>>>   print("Allowed to drive")
>>> else:
>>>   print("Not allowed to drive")
Not allowed to drive
```
A good though not perfect example on the use of "truthy" and "falsy" application is in forms or in keying in log in credentials.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fbtxcl1k3i8rg8cmnqj4.png)

When a field is set to as 'a required field', the system expects the field to be "truthy" hence should not be left blank as this will lead to it being assigned "falsy".

- Ternary Operator (Conditional Expression)

This is another way to do conditional logic. This works the same as 'if statements' but can in a way be referred to as a 'shortcut' so can only be used in certain conditional logic.
In this mode, we start with the condition then incorporate the "if statement".
`"condition_if_true" if condition else "condition_if_false"`

Example:
Let's use an example to determine if a user is your friend (eg. on Facebook whether the user can message you).
```
>>> is_friend = True
>>> can_message = "Message allowed" if is_friend else "not allowed to message"
>>> print(can_message)
Message allowed

>>> is_friend = True
>>> can_message = "Message allowed" if is_friend else "not allowed to message"
>>> print(can_message)
not allowed to message
```

- Short Circuiting

Previously we saw how to use the 'and' keyword to validate whether both statements are True:
```
>>> is_friend = True
>>> is_user = True
>>> print(is_friend and is_user)
True

>>> is_friend = True
>>> is_user = False
>>> print(is_friend and is_user)
False
```
In short circuiting, the interpreter ignores one part of the condition(despite it being false) and returns either True or False.
Example, using the 'or' keyword, if the first part of the condition is True the interpreter returns True without checking the second part.
When we use the 'and' keyword, when the the first part of the statement is False, the interpreter ignores(short circuits) the second part and returns False.
An example using the 'or' keyword:

```
>>> is_friend = True
>>> is_user = False
>>> print(is_friend or is_user)
True

>>> is_friend = True
>>> is_user = True
>>> print(is_friend or is_user)
True

>>> is_friend = False
>>> is_user = True
>>> if is_friend or is_user:
>>>   print("Best friends forever")
Best friends forever

>>> is_friend = False
>>> is_user = True
>>> if False or is_user:
>>>   print("Best friends forever")
>>> else:
>>>   print("Never friends")
Best friends forever

>>> is_friend = True
>>> is_user = False
>>> if False or is_user:
>>>   print("Best friends forever")
>>> else:
>>>   print("Never friends")
Never friends

>>> if True or True:
>>>   print("Best friends forever")
>>> else:
>>>   print("Never friends")
Best friends forever
```

- Logical Operators

We have looked at a few logical operators previously `'and'` and `'or'`. A _logical operator_ allows us to perform logic between two things.
Other logical operators include:

- Greater than `>`

- Less than `<`

- Equal to `==`

- Greater than or equal to `>=`

- Less than or equal to `<=`

- Not equal to `!=` _(Opposite of equal to)_

- `not` keyword / Function - It negates the statement. Can also be written with bracket: `not()`.

They return either True or False:

```
>>> print(4 > 5)
False

>>> print(4 < 5)
True

>>> print(4 == 5)
False

>>> print(1 >= 0)
True

>>> print(1 <= 0)
False

>>> print(0 >= 0)
True

>>> print(0 != 0)
False

>>> print(not(True))
False

>>> print(not True)
False

>>> print(not False)
True

>>> print(not(1 == 1))
False

>>> print(not(1 != 1))
True
```
`Note:` A single equal sign `=` symbol is used in assigning values to variables hence to use the "equal to" operator for comparison, we use a double `==` symbol.

```
>>> print('a' > 'b')
False

>>> print('a' > 'A')
True
```
`But why/how is 'a' > 'b' False; and 'a' > 'A' True?`

In the case of strings, Python compares the ASCII values of the characters. Hence, 'a' ASCII value is 97, 'b' is 98 and 'A' ASCII value is 65 that's why 'a' is greater than 'A' and 'b' is greater than 'a'.

**`*optional`**
In the case of, `print('abc' < 'bac')`, the result will be `True`. _(Though this is a bit beyond the scope of the course)_. 
This kind of comparison uses lexicographical ordering: first the first two items are compared, and if they differ this determines the outcome of the comparison; if they are equal, the next two items are compared, and so on, until either sequence is exhausted.

> Lexicographical ordering for strings uses the Unicode code point number to order individual characters.

```
>>> print(1 < 2 < 3 < 4)
True

>>> print(1 < 2 > 3 < 4)
False
```

- **_Exercise1_** _(Done below)_

You have been hired by a gaming company to create a program for a game where the character has magic powers and is an expert.
If the character has magic and is an expert, the output should be "You are a master magician". Otherwise, if the character has magic but is not an expert, the output should be "At least you're getting there". Else, if the character has no magic, the output should be "You need magic powers".
 

```
>>> print("Enter '1'(Yes) or '0'(No) for each question.")
>>> has_magic = bool(int(input("Does the character has magic? ")))
>>> is_expert = bool(int(input("Is the character an expert? ")))

>>> if has_magic and is_expert:
>>>   print("You are a master magician.")
>>> elif has_magic and not is_expert:
>>>   print("At least you're getting there.")
>>> elif not has_magic:
>>>  print("You need magic powers.")
Enter '1'(Yes) or '0'(No) for each question.
Does the character has magic? _1_
Is the character an expert? _1_
You are a master magician.

(Re-run the program)
Enter '1'(Yes) or '0'(No) for each question.
Does the character has magic? _0_
Is the character an expert? _1_
You need magic powers.

(Re-run the program)
Enter '1'(Yes) or '0'(No) for each question.
Does the character has magic? _1_
Is the character an expert? _0_
At least you're getting there.
```

- 'is' keyword

Unlike the double equal sign `==`, which compares the equality in values, `is` is a keyword that checks if the location in memory where one value is stored is the same as the other's.
Example:

```
>>> print(True == 1)
>>> print('' == 1)
>>> print('1' == 1)
>>> print([] == 0)
>>> print(10 == 10.0)
>>> print([] == [])
>>> print([1, 2, 3] == [1, 2, 3])
True
False
False
False
True
True
True

>>> print(True is 1)
>>> print('' is 1)
>>> print('1' is 1)
>>> print([] is 0)
>>> print(10 is 10.0)
>>> print([] is [])
>>> print([1, 2, 3] is [1, 2, 3])
False
False
False
False
False
False
False

>>> print(True is True)
True

>>> print('1' is '1')
True

>>> print(10 is 10)
True
```
Once a list is created, it is stored in different memory space hence `print([] is [])` or `print([1, 2, 3] is [1, 2, 3])` will always evaluate to False.

> All _Data Structures_ in Python are stored in different memory locations.

---

- For Loops

**Loops** are one of the most powerful features of a programming languages. The concept of looping allows us to run lines of code over and over till we accomplish a specific task.
In creating a `for loop`, we use the keyword `'for'`.
Example: `for i in 'name':`
'i' in the loop is a _variable_ for each element in the loop and can be any different name: `for item in 'name':`, `for teddy in 'name':` and is created for each item in 'name'(iterable).
An _iterable_ is something that can be looped over.

```
>>> for item in 'name':
>>>    print(item)
n
a
m
e

>>> for item in [1, 2, 3, 4]:
>>>    print(item)
1
2
3
4

>>> name = 'Mark'
>>> for i in name:
>>>    print(i)
M
a
r
k

>>> for item in {1, 2, 3, 4}:
>>>    print(item)
1
2
3
4

>>> for item in (1, 2, 3, 4):
>>>    print(item)
1
2
3
4

>>> for item in (1, 2, 3, 4):
>>>    print(item)
>>>    print(item)
>>>    print(item)
>>> print("Hello")

1
1
1
2
2
2
3
3
3
4
4
4
Hello

>>> for item in (1, 2, 3, 4):
>>>    print(item)
>>>    print(item)
>>>    print(item)
>>> print(item)
1
1
1
2
2
2
3
3
3
4
4
4
4
```

- Nested for loops

```
>>> for item in (1, 2, 3, 4, 5):
>>>     for x in ['a', 'b', 'c']:
>>>         print(item, x)
1 a
1 b
1 c
2 a
2 b
2 c
3 a
3 b
3 c
4 a
4 b
4 c
5 a
5 b
5 c
```

- Iterables

An iterable is an object or a collection that can be _iterated_ over (looped over).
An iterable can be a list, tuple, dictionary, set and string. This means that one can go one by one checking each item in the collection.

- `Iterating over a dictionary`

```
>>> user = {
>>>     'name' : 'Mark',
>>>     'age' : 30,
>>>     'can_swim' : False
>>>   }

>>> for item in user:
>>>     print(item)
name
age
can_swim
```
When we iterate over a dictionary we only get the keys but can use the dictionary methods to loop over the dictionary items which includes its values.

- One is `'x.items()'` where we get the key-value pairs in tuples form.

```
>>> user = {
>>>     'name' : 'Mark',
>>>     'age' : 30,
>>>     'can_swim' : False
>>>   }

>>> for item in user.items():
>>>     print(item)
('name', 'Mark')
('age', 30)
('can_swim', False)
```

- Second is `'x.values()'` where we get only the values in the dictionary.

```
>>> user = {
>>>     'name' : 'Mark',
>>>     'age' : 30,
>>>     'can_swim' : False
>>>   }

>>> for item in user.values():
>>>     print(item)
Mark
30
False
```

- Third is `'x.keys()'` where we get only the keys in the dictionary. Works the same as iterating the dictionary without including a method.

```
>>> user = {
>>>     'name' : 'Mark',
>>>     'age' : 30,
>>>     'can_swim' : False
>>>   }

>>> for item in user.keys():
>>>     print(item)
name
age
can_swim
```

> What if you want to print the items (key and values) in the dictionary separately? We can use `tuple unpacking`.

```
>>> user = {
>>>     'name' : 'Mark',
>>>     'age' : 30,
>>>     'can_swim' : False
>>>   }

>>> for item in user.items():
>>>     key, value = item
>>>     print(key, value)
name Mark
age 30
can_swim False

(second way of unpacking)

>>> user = {
>>>     'name' : 'Mark',
>>>     'age' : 30,
>>>     'can_swim' : False
>>>   }

>>> for key, value in user.items():
>>>     print(key, value)
name Mark
age 30
can_swim False

```
- **_Exercise2_** _(Done below)_

Building a simple 'counter' to loop over a list and sum up the items in the list. The list is provided below.
`my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`

```
>>> my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> sum = 0
>>> for i in my_list:
>>>     sum += i
>>> print (sum)
55
```

- range() in loops

It returns an object that produces a sequence of integers from the start (which is inclusive) to stop (exclusive).

```
>>> print(range(100))
range(0, 100)

>>> print(range(0, 100))
range(0, 100)
```
We can iterate a range of numbers.

```
>>> for num in range(20)
>>>    print(num)
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

>>> for i in range(2, 15)
>>>    print(i)
2
3
4
5
6
7
8
9
10
11
12
13
14

>>> for i in range(10):
>>>    print('my name is')
my name is
my name is
my name is
my name is
my name is
my name is
my name is
my name is
my name is
my name is
```
When one _'does not want to use'_ a variable name in the loop, the person can use an underscore `_`:

```
>>> for _ in range(20)
>>>    print(_)
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

>>> for _ in range(10):
>>>    print('my name is')
my name is
my name is
my name is
my name is
my name is
my name is
my name is
my name is
my name is
my name is
```

- (start: stop: stepover) in range()

```
>>> for _ in range(0, 10, 2)
>>>    print(_)
0
2
4
6
8

>>> for _ in range(0, 10, -1)
>>>    print(_)
#nothing will be printed out

>>> for _ in range(10, 0, -1)
>>>    print(_)
10
9
8
7
6
5
4
3
2
1

>>> for _ in range(10, 0, -2)
>>>    print(_)
10
8
6
4
2

>>> for _ in range(2)
>>>    print(list(range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

- enumerate()

It returns each item in the iterable with its index in tuple form.

```
>>> for i in enumerate('mark'):
>>>    print(i)
(0, 'm')
(1, 'a')
(2, 'r')
(3, 'k')

>>> for i, j in enumerate('mark'):
>>>    print(i, j)
0 m
1 a
2 r
3 k

>>> for i, char in enumerate(list(range(100))):
>>>     if char == 50:
>>>         print(f"The index of 50 is: {i}")
The index of 50 is: 50

>>> for i, j in enumerate('Mark'):
>>>     if j == 'r':
>>>         print(f"The index of r is: {i}")
The index of r is: 2
```

---

- While Loops

In `While loop`, a command is run when a specific condition is met till the condition becomes false after constant looping.
eg:

```
>>> i = 0
>>> while 0 < 50:
>>>     print(i)
0
0
0
0
0
#will run infinitely for 0 will always be less than 50.

>>> i = 0
>>> while i < 50:
>>>     i += 5
>>>     print(i)
5
10
15
20
25
30
35
40
45
50
```

- break command in while loop

When a `break` keyword is used in `while loop`, it breaks the loop after the first run.

```
>>> i = 0
>>> while 0 < 50:
>>>     print(i)
>>>     break
0
```

- using else in while loop

```
>>> i = 0
>>> while i < 50:
>>>     i += 5
>>>     print(i)
>>> else:
>>>     print("Done with work")
5
10
15
20
25
30
35
40
45
50
Done with work

>>> i = 0
>>> while i > 50:
>>>     i += 5
>>>     print(i)
>>> else:
>>>     print("Done with work")
Done with work
```
The `else` block in `while loop` will only execute when there is no `break` statement in the `while loop`.

```
>>> i = 0
>>> while i < 50:
>>>     i += 5
>>>     print(i)
>>>     break
>>> else:
>>>     print("Done with work")
0
```

- How `for loops` and `while loops` relates

```
>>> my_list = [1, 2, 3]
>>> for item in my_list:
>>>     print(item)
1
2
3
------------------------------------
>>> my_list = [1, 2, 3]
>>> i = 0
>>> while i < len(my_list):
>>>     print(my_list[i])
>>>     i += 1 
1
2
3
```
`While loops` are more flexible for we have a conditional statement but `for loops` are simpler. With the `while loop`, we have to remember to hope the loop in the course or use a break statement, to avoid getting an infinite loop.

```
>>> while True:
>>>     input("Say something: ")
>>>     break
Say something: _hi_

>>> while True:
>>>     response = input("Say something: ")
>>>     if (response == "bye"):
>>>         break
Say something: _hi_
Say something: _hi_
Say something: _bye_
```

- break, continue, pass

The `break` keyword breaks out of the loop. The `continue` keyword continues the loop till the condition is met without running the indented line(s) below it. The `pass` keyword is used to pass to the next line. It is mainly used as a placeholder.

```
>>> my_list = [1, 2, 3]
>>> for item in my_list:
>>>    continue
>>>    print(item)
#nothing will be printed

>>> i = 0
>>> while i < len(my_list):
>>>    i += 1
>>>    continue
>>>    print(my_list[i])
#nothing will be printed

>>> my_list = [1, 2, 3]
>>> for item in my_list:
>>>    pass

>>> i = 0
>>> while i < len(my_list):
>>>    print(my_list[i])
>>>    i += 1
>>>    pass
1
2
3
```

- **_Exercise3_** _(Done below)_

> Our First GUI Exercise 🥳 (Basic Version)

In this exercise, we are going to simulate what the computer does when we have a graphical user interface (GUI).

_Assuming below are the pixels of an image (Christmas Tree in a nested list),_;

```
picture = [
   [0, 0, 0, 1, 0, 0, 0],
   [0, 0, 1, 1, 1, 0, 0],
   [0, 1, 1, 1, 1, 1, 0],
   [1, 1, 1, 1, 1, 1, 1],
   [0, 0, 0, 1, 0, 0, 0],
   [0, 0, 0, 1, 0, 0, 0]
]
```
You gonna loop over the list(s) and the moment you encounter a zero(0), you will display on the screen 'an empty space' but when you encounter a one(1), you gonna simulate a pixel hence display a star(*).

_For this challenge, we will include a new special print parameter(option) known as '`end =`'_. 
By default, the `print` function ends with a newline (when we print anything, we get a new line for the next statement). Passing the whitespace to the end parameter `(end=' ')` indicates that the end character has to be identified by whitespace and not a newline which we will use, for in this case, we don't want a new line after printing each and every character.
`end =` - "String appended after the last value".

```
(Without using the 'end' parameter)

>>> picture = [
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 1, 1, 1, 1, 1, 0],
       [1, 1, 1, 1, 1, 1, 1],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0]
    ]

>>> for row in picture:
>>>    for pixel in row:
>>>       if pixel == 0:
>>>          print(" ")
>>>       else:
>>>          print("*")
 
 
 
*
 




*
*
*



*
*
*
*
*

*
*
*
*
*
*
*



*





 
*




```
(After using the end parameter to include a blank space 'empty string' after the end of the line)

```
>>> picture = [
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 1, 1, 1, 1, 1, 0],
       [1, 1, 1, 1, 1, 1, 1],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0]
    ]

>>> for row in picture:
>>>    for pixel in row:
>>>       if pixel == 0:
>>>          print(" ", end = ' ')
>>>       else:
>>>          print("*", end = ' ')

      *           * * *       * * * * *   * * * * * * *       *             *       
```
(After each loop of the pixels, we also wanna include a blank space 'empty string' so that each single loop will be by default in each line):

```
>>> picture = [
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 1, 1, 1, 1, 1, 0],
       [1, 1, 1, 1, 1, 1, 1],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0]
    ]

>>> for row in picture:
>>>    for pixel in row:
>>>       if pixel == 0:
>>>          print(" ", end = ' ')
>>>       else:
>>>          print("*", end = ' ')
>>>    print(" ")

      *        
    * * *      
  * * * * *
* * * * * * *
      *
      *
```

- **_Exercise4_** _(Done below)_

(Finding Duplicates in a list using loops and conditional logic)
`Note:` No use of sets in this case.
After finding the duplicate values, the program should print the duplicate values.

```
>>> some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']
>>> duplicates = []
>>> for value in some_list:
>>>    if some_list.count(value) > 1:
>>>        if value not in duplicates:
>>>            duplicates.append(value)
>>> print(duplicates)
['b', 'n']
```

---

- Functions

Up until now, we've worked with python functions like `print`, `list`, `input` function and many more that allowed us to perform actions on our data types.
We can also create our own functions and use them on our program.
When creating functions in Python we use the `def` - 'define' keyword. We then give our function a name (defining the function) as we do with variables then we add the brackets () and a colon at the end.
_For one to use a function one has to 'call it'._

```
>>> def say_hello(): #defining the function
>>>    print("Hellooo")
>>> say_hello() #calling the function
Hellooo
```
Functions are super useful because the work under the principle of 'DRY - Don't Repeat Yourself' because instead of the programmer re-typing the code each and every time, the programmer can just call the function as many times as possible to run a specific block of code.

Example: Using our Christmas tree example above, we can use the function to output it multiple of times.

```
>>> picture = [
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 1, 1, 1, 1, 1, 0],
       [1, 1, 1, 1, 1, 1, 1],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0]
    ]

>>> def show_tree():
>>>    for row in picture:
>>>        for pixel in row:
>>>            if pixel == 0:
>>>                print(" ", end = ' ')
>>>            else:
>>>                print("*", end = ' ')
>>>        print(" ")

>>> show_tree()
>>> show_tree()
>>> show_tree()

      *        
    * * *      
  * * * * *
* * * * * * *
      *
      *
      *
    * * *
  * * * * *
* * * * * * *
      *
      *
      *
    * * *
  * * * * *
* * * * * * *
      *
      *
```

> The function is stored in a specific place in memory once created.

```
>>> def say_hello(): 
>>>    print("Hellooo")
>>> print(say_hello)
<function say_hello at 0x000002332BB33E20>

#The characters '0x000002332BB33E20' show the memory location where the function has been stored.
```

- Arguments Vs Parameters(in functions)

The power of functions beyond it being able to be called multiple times, is the ability of the programmer to make it dynamic. In its brackets, one can pass parameters.
The values which are defined at the time of the _function prototype_ or _definition of the function_ are called as **<u>parameters</u>**.
When a function is 'called', the actual values that are passed during the 'call' are called as **<u>arguments</u>**. 

```
>>> def say_hello(name, age): #name and age are parameters.
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello("Mark", 20) #"Mark" and 20 are arguments.
Hello Mark, You're 20 yrs

>>> def say_hello(name, age):
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello("Mark", 20)
>>> say_hello("Emily", 19)
>>> say_hello("Dan", 17)
Hello Mark, You're 20 yrs
Hello Emily, You're 19 yrs
Hello Dan, You're 17 yrs
```
The above arguments are referred to as **<u>positional arguments</u>** because they are required to be in the proper position.

```
>>> def say_hello(name, age):
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello(20, "Mark")
Hello 20, You're Mark yrs
```

- Default Parameters and Keyword Arguments

`Keyword arguments`, as opposed to `positional arguments`, allow us to not worry about the position hence the arguments can be in any position.
_However this makes the code more complicated and not a proper practice way._ 
```
>>> def say_hello(name, age):
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello(age = 20, name = "Mark")
Hello Mark, You're 20 yrs
```

`Default parameters` allow us to give constant values as we define the function. Default parameters **only** work when no values have been passed as arguments to the function.

```
>>> def say_hello(name = "Emily", age = 17):
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello()
Hello Emily, You're 17 yrs

>>> def say_hello(name = "Emily", age = 17):
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello("Dan", 23)
>>> say_hello()
Hello Dan, You're 23 yrs
Hello Emily, You're 17 yrs

>>> def say_hello(name = "Emily", age = 17):
>>>    print(f"Hello {name}, You're {age} yrs")
>>> say_hello("Irene")
Hello Irene, You're 17 yrs
```

- Return Statement

This is a keyword in python mostly used together with functions.
Functions always have to return something and when there is no `return statement`, the function will always return `None`.

```
>>> def sum(num1, num2):
>>>    num1 + num2
>>> print(sum(4, 5))
None
```
When the return statement is used;

```
>>> def sum(num1, num2):
>>>    return num1 + num2
>>> print(sum(4, 5))
9
```

> A function should do one thing really well and/or should return something. This however doesn't mean that the code only has to be one line.

```
>>> def sum(num1, num2):
>>>    return num1 + num2
>>> total = sum(10, 5)
>>> print(sum(10, total))
25

>>> def sum(num1, num2):
>>>    return num1 + num2
>>> print(sum(10, sum(10, 5)))
25

>>> def sum(num1, num2):
>>>    def another_func(num1, num2):
>>>       return num1 + num2
>>> total = sum(10, 20)
>>> print(total)
None

def sum(num1, num2):
   def another_func(num1, num2):
      return num1 + num2
   return another_func
total = sum(10, 20)
print(total)
<function sum.<locals>.another_func at 0x000002387BF49B40>

>>> def sum(num1, num2):
>>>    def another_func(num1, num2):
>>>       return num1 + num2
>>>    return another_func
>>> total = sum(10, 20)
>>> print(total(10, 20))
30

>>> def sum(num1, num2):
>>>    def another_func(num1, num2):
>>>       return num1 + num2
>>>    return another_func(num1, num2)
>>> total = sum(10, 20)
>>> print(total)
30
```
To avoid confusion when working with more than one function (function in a function), it is advisable to use different names for the parameter.

```
>>> def sum(num1, num2):
>>>    def another_func(n1, n2):
>>>       return num1 + num2
>>>    return another_func(num1, num2)
>>> total = sum(10, 20)
>>> print(total)
30
```
`Note:` A `return` keyword automatically exits the function in that any code (to output) below the return statement is never run.

```
>>> def sum(num1, num2):
>>>    def another_func(n1, n2):
>>>       return num1 + num2
>>>    return another_func(num1, num2)
>>>    return 5
>>>    print("Hello")
>>> total = sum(10, 20)
>>> print(total)
30
```

- Methods Vs Functions

Examples of inbuilt functions in python include : `list()`, `print()`, `max()`, `min()`, `input()`. We've also found out that we can use the keyword `def` to define our own functions.

When using methods, we use the dot(.) notation. Methods are owned by 'whatever is to the left of the dot(.)' be it strings, tuples, integers etc. Methods of the fundamental data types have been covered in the [previous module](https://dev.to/gateremark/python-from-the-word-go-4h93) where all the data types have been covered too.
Both Functions and Methods allow us to take actions on the data types.
`None:` Similar to functions, we can also build our own methods which will be explained in details in the next module as we discuss on 'classes and objects'.

- Docstrings

This is using triple quotes ('''...''') to 'comment' multiple lines. It can also be used in functions to give more info about a function.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iidvnocgqjgmzsb78q12.png)

It works the same as the more info about a function provided by the developer environments when typing the inbuilt functions.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6meal4tovxblriz8ao9.png)

- Help function

Used to give more info about a function. When a docstring is passed in a function, the help function returns the docstring.

```
>>> help(print)

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

-----------------------------------------------------------------

>>> def test(a):
       '''
       Info: This is a function that prints the testing data.
       '''
       print(a)
>>> help(test)
test(a)
    Info: This is a function that prints the testing data.
```

We can also use the dunder method (Magic method) _'will get into it later in the course'_ to get more info on a function.

```
>>> def test(a):
       '''
       Info: This is a function that prints the testing data.
       '''
       print(a)
>>>print(test.__doc___)
Info: This is a function that prints the testing data.
```

> Docstrings are really useful to add comments and definition to a function to enable other people understand what your function does without searching through your multiple files.

- Writing clean code

```
>>> def is_even(num):
>>>    if num % 2 == 0:
>>>      return True
>>>    elif num % 2 != 0:
>>>      return False
>>> print(is_even(50))
True
```
Vs

```
>>> def is_even(num):
>>>    if num % 2 == 0:
>>>      return True
>>>    else:
>>>      return False
>>> print(is_even(50))
True
```
Vs

```
>>> def is_even(num):
>>>    if num % 2 == 0:
>>>      return True
>>>    return False
>>> print(is_even(50))
True
```
Vs

```
>>> def is_even(num):
>>>    return num % 2 == 0
>>> print(is_even(50))
True
```

- *args and **kwargs

*args - arguments
**kwargs - Keyword arguments

In function we have special characters called `*args` and `**kwargs`.

```
>>> def super_func(num):
>>>    return sum(num)
>>> super_func(1, 2, 3, 4, 5)
#returns an error because the function should take just 1 positional argument but we gave 5.
```
**_args (adding an asterisk at the start of the parameter) allows one to pass more than one positional argument.**

```
>>> def super_func(*num):
>>>    print(*num)
>>>    return sum(num)
>>> super_func(1, 2, 3, 4, 5)
1 2 3 4 5

>>> def super_func(*num):
>>>    print(num)
>>>    return sum(num)
>>> super_func(1, 2, 3, 4, 5)
(1 2 3 4 5)

>>> def super_func(*num):
>>>    return sum(num)
>>> print(super_func(1, 2, 3, 4, 5))
15
```
**__kwargs allows us to use keyword arguments.** It returns a dictionary of the values.

```
>>> def super_func(*num, **kwargs):
>>>    print(kwargs)
>>> super_func(1, 2, 3, 4, 5, num1=10, num2=15)
{'num1': 10, 'num2': 15}

>>> def super_func(*num, **kwargs):
>>>    print(kwargs)
>>>    total = 0
>>>     for items in kwargs.values():
>>>        total += items
>>>     return sum(num) + total
>>> print(super_func(1, 2, 3, 4, 5, num1=10, num2=15))
{'num1': 10, 'num2': 15}
40
```
There's a rule of positioning when one is using parameters, default parameters, *args and **kwargs:

> parameters, *args, default parameters, **kwargs

```
>>> def super_func(name, *num, greet="hi", **kwargs):
>>>    print(kwargs)
>>>    total = 0
>>>     for items in kwargs.values():
>>>        total += items
>>>     return sum(num) + total
>>> print(super_func("Andy", 1, 2, 3, 4, 5, num1=10, num2=15))
{'num1': 10, 'num2': 15}
40
```

- **_Exercise5_** _(Done below)_

Create a function called 'highest_even' that is passed a list of integers and returns the highest even integer.
The list is given below:
`my_list = [10, 2, 3, 8, 4, 11]`

```
>>> my_list = [10, 2, 3, 4, 8, 11]
>>> even_list = []
>>> def highest_even():
>>>    for i in my_list:
>>>       if i % 2 == 0:
>>>          even_list.append(i)
>>>          even_list.sort()
>>>          print(even_list)
>>>    print(even_list[-1])
>>> highest_even()
[10]
[2, 10]
[2, 4, 10]
[2, 4, 8, 10]
10

-------------------------------------------------------------

>>> my_list = [10, 2, 3, 4, 8, 11]
>>> even_list = []
>>> def highest_even():
>>>    for i in my_list:
>>>       if i % 2 == 0:
>>>          even_list.append(i)
>>>          even_list.sort()
>>>    print(even_list[-1])
>>> highest_even()
10
```
We can also use the `max` keyword to return the maximum even number:

```
>>> my_list = [10, 2, 3, 4, 8, 11]
>>> def highest_even():
>>>    even_list = []
>>>    for i in my_list:
>>>       if i % 2 == 0:
>>>          even_list.append(i)
>>>    return max(even_list)
>>> print(highest_even())
10
```

- Walrus Operator (:=)

The `walrus operator` is a new feature in `python 3.8`.
It is explained in depths in [the python documentation](https://docs.python.org/3/whatsnew/3.8.html). 
It is used to assign values to variables as part of a 'larger expression' eg. in `if statements`, in `while loops` etc.

```
>>> a = "Helloooooooooooooooo"
>>> if (len(a) > 10):
>>>     print(f"Too long, {len(a)} elements")
Too long, 20 elements
```
Using the walrus operator:

```
>>> a = "Helloooooooooooooooo"
>>> if ((n := len(a)) > 10):
>>>     print(f"Too long, {n} elements")
Too long, 20 elements
```
_Walrus Operator (while loop)_

```
>>> a = "Helloooooooooooooooo"
>>> while ((n := len(a)) > 1):
>>>     print(n)
>>>     a = a[:-1]
>>> print(a)
20
19
18
17
16
15
14
13
12
11
10
9
8
7
6
5
4
3
2
H

-----------------------------------------------------------------

>>> a = "Helloooooooooooooooo"
>>> while ((n := len(a)) > 1):
>>>     print(a)
>>>     a = a[:-1]
>>> print(a)
Helloooooooooooooooo
Hellooooooooooooooo
Helloooooooooooooo
Hellooooooooooooo
Helloooooooooooo
Hellooooooooooo
Helloooooooooo
Hellooooooooo
Helloooooooo
Hellooooooo
Helloooooo
Hellooooo
Helloooo
Hellooo
Helloo
Hello
Hell
Hel
He
H
```

---

- Scope 

"What variables do I have access to?"

```
>>> print(name)
#returns an error; NameError: name 'name' is not defined
```
Once a variable is not defined, one cannot have access to it.
A variable with a **global scope** is a variable that can be accessed by anybody in the file and can be used anywhere; in the conditional logic; in the while loop; in a function, etc.

```
>>> total = 100
>>> print(total)
100

#total has a global scope
```
A variable with a **functional scope** is a variable that can only be accessed in within the function.

```
>>> def some_func():
>>>    total = 100
>>> print(total)
#returns an error; NameError: name 'total' is not defined

>>> def some_func():
>>>    total = 100
>>>    print(total)
100
```

- Scope Rules

"What would you expect the output of the code below to be?"

```
>>> a = 1
>>> def confusion():
>>>   a = 5
>>>   return a
>>> print(a)
>>> print(confusion())
```
The output will be `1` and `5`. This is because the first print function, `print(a)`, outputs the value stored in the `a` with the global scope while the second print function, `print(confusion())`, returns the value stored in the `a` variable in the function.
The `a` is not modified after the function because of `scope`.

Rules the interpreter follow in scope:
#####1. Local Scope - Scope within the functional scope.

```
>>> a = 1
>>> def confusion():
>>>   a = 5
>>>   return a
>>> print(confusion())
>>> print(a)
5
1

>>> a = 1
>>> def confusion():
>>>   return a
>>> print(confusion())
>>> print(a)
1
1
```
#####2. Parent Local scope - Works where there's a function within a function.

```
>>> a = 1
>>> def parent():
>>>   a = 10
>>>   def confusion():
>>>      return a
>>>   return confusion()
>>> print(parent())
>>> print(a)
10
1
```
#####3. Global scope 

```
>>> a = 1
>>> def parent():
>>>   def confusion():
>>>      return a
>>>   return confusion()
>>> print(parent())
>>> print(a)
1
1
```
#####4. Built in python functions

```
>>> a = 1
>>> def parent():
>>>   def confusion():
>>>      return sum
>>>   return confusion()
>>> print(parent())
>>> print(a)
<built-in function sum>
1
```
`Note:` Parameters are part of the local scope:

```
>>> b = 10
>>> def confusion(b):
>>>    print(b)
>>> confusion(300)
300

>>> b = 10
>>> def confusion(b):
>>>    print(b)
>>> confusion(b)
10
```

- Global keyword

"What if one wants to refer to a global variable while in the function without creating a new variable?"
Example:

```
>>> total = 0
>>> def count():
>>>    total += 1
>>>    return total
>>> print(count())
#returns an error; UnboundLocalError: local variable 'total' referenced before assignment
```
The error occurs because the function `count` does not recognize `total`. Hence, we have to add a variable `total` in the function:

```
>>> total = 0
>>> def count():
>>>    total = 0
>>>    total += 1
>>>    return total
>>> print(count())
1
```
To avoid recreating another variable in the function we can use the `global keyword` to tell the interpreter that we want to use the global scope on the variable in the function.

```
>>> total = 0
>>> def count():
>>>    global total
>>>    total += 1
>>>    return total
>>> print(count())
1

>>> total = 0
>>> def count():
>>>    global total
>>>    total += 1
>>>    return total
>>> count()
>>> count()
>>> print(count())
3
```
- Dependency injection (A simplified version of global scope)

```
>>> total = 0
>>> def count(total):
>>>    total += 1
>>>    return total
>>> print(count(total))
1

>>> total = 0
>>> def count(total):
>>>    total += 1
>>>    return total
>>> print(count(count(count(total))))
3
```

- Nonlocal Keyword

This is a new keyword(feature) in python 3 and its used to refer to the parent local scope.

```
>>> def outer():
>>>    x = "local"
>>>    def inner():
>>>       nonlocal x
>>>       x = "nonlocal"
>>>       print("inner:", x)
>>>    inner()
>>>    print("outer:", x)
>>> outer()
inner: nonlocal
outer: nonlocal

#Without the nonlocal keyword

>>> def outer():
>>>    x = "local"
>>>    def inner():
>>>       x = "nonlocal"
>>>       print("inner:", x)
>>>    inner()
>>>    print("outer:", x)
>>> outer()
inner: nonlocal
outer: local
```

> Why do we need scope?

"Why not just have every variable with a global scope so that everything has access to everything?"
Machines don't have infinite power/memory hence we need to be cautious of the resources we use. Scope is a good demonstration of this.
Eg. When a function is run, we create one memory space hence when for instance we use nonlocal, we instead of creating another memory space, we use an existing one.

---

Hurraay!! 🥳
We come to the end of the second module. Hope you've learnt a lot and ready to implement the skills in different fields. 
As we wrap up, here is a simple exercise for you to try before checking the answer done below:

```
>>> age = input("What is your age?: ")
>>> if int(age) < 18:
>>>     print("Sorry, you are too young to drive this car. Powering off!")
>>> elif int(age) > 18:
>>>     print("Powering On. Enjoy the ride!");
>>> else:
>>>     print("Congratulations on your first year of driving. Enjoy the ride!")
```
Given the code above, perform the actions below on the code:

1. Wrap the above code in a function called checkDriverAge() that whenever you call this function, you will get prompted for age.

2. Instead of using the input(), make the checkDriverAge() function accept an argument of age, so that if you enter eg. checkDriverAge(92), it returns "Powering On. Enjoy the ride!"

3. Make the default age set to 0 if no argument is given.

Answer:

```
>>> def checkDriverAge(age=0):
>>>    if int(age) < 18:
>>>        print("Sorry, you are too young to drive this car. Powering off")
>>>    elif int(age) > 18:
>>>        print("Powering On. Enjoy the ride!");
>>>    else:
>>>        print("Congratulations on your first year of driving. Enjoy the ride!")
>>> checkDriverAge(10)
Sorry, you are too young to drive this car. Powering off
```

---

What next? 🤔
Let's meet in the next module `#Advanced Python1`. 

Till next time; bye bye.