# Practice

## Control statements and looping

### `if`, `else`, and `elif`

Let’s practice our branching statements. Remember that `elif` (short for `else if`) is an optional branch that will let you add another `if` test, and `else` is an optional branch that will catch anything not previously caught by `if` or `elif`.

``````>>> def test_number(number):
...     if number < 100:
...         print("This is a pretty small number")
...     elif number == 100:
...         print("This number is alright")
...     else:
...         print("This number is huge!")
...
>>> test_number(5)
>>> test_number(99)
>>> test_number(100)
>>> test_number(8675309)
``````
Here's what you should have seen in your REPL:

You can also have multiple conditions in an if statement. This function prints “Fizzbuzz!” if the number is divisible by both 3 and 5 (the `%` or modulo operator returns the remainder from the division of two numbers):

``````>>> def fizzbuzz(number):
...     if number % 3 == 0 and number % 5 == 0:
...         print("Fizzbuzz!")
...
>>> fizzbuzz(3)
>>> fizzbuzz(5)
>>> fizzbuzz(15)
``````
Here's what you should have seen in your REPL:

Let’s also practice using `if` to test for an empty list. Remember that an empty list is “Falsey”, or resolves to `False`. Write a function to print a list of elements, or an error message if the list is empty. Print a special message if a list item is `None`:

``````>>> def my_func(my_list):
...     if my_list:
...         for item in my_list:
...             if item is None:
...                 print("Got None!")
...             else:
...                 print(item)
...     else:
...         print("Got an empty list!")
...
>>> my_func([1, 2, 3])
1
2
3
>>> my_func([2, None, "hello", 42])
2
Got None!
hello
42
>>> my_func([])
Got an empty list!
``````

## The `for` loop, `range()` and `enumerate()`

Let’s try making a list and looping over it:

``````>>> my_list = [0, 1, 2]
>>> for num in my_list:
...     print(f"Next value: {num}")
...
``````

If we’re just interested in looping over a list of numbers, we can use the `range()` function instead. Remember that the first argument is inclusive and the second is exclusive:

``````>>> for num in range(0, 3):
...     print(f"Next value: {num}")
...
``````

Another useful function is `enumerate()`, which iterates over an iterable (like a list) and also gives you an automatic counter. `enumerate()` returns a tuple in the form of (`counter`, `item`).

``````>>> my_list = ["foo", "bar", "baz"]
>>> for index, item in enumerate(my_list):
...     print(f"Item {index}: {item}")
...
``````

We can also loop over a dictionary’s keys and/or values. If you try to iterate over the dictionary object itself, what do you get?

``````>>> my_dict = {"foo": "bar", "hello": "world"}
>>> for key in my_dict:
...     print(f"Key: {key}")
...
# This is equivalent to...
>>> for key in my_dict.keys():
...     print(f"Key: {key}")
...
``````

The `keys()` method returns the dictionary’s keys as a list, which you can then iterate over as you would any other list. This also works for `values()`

``````>>> for value in my_dict.values():
...     print(f"Value: {value}")
...
``````

The most useful function, however, is `items()`, which returns the dictionary’s items as tuples in the form of (key, value):

``````>>> for key, value in my_dict.items():
...     print(f"Item {key} = {value}")
...
``````
Here's what you should have seen in your REPL:

## `break`, `continue`, and `return`

`break` and `continue` are important functions for controlling the program flow inside loops. `break` ends the loop immediately and continues executing from outside the loop’s scope, and `continue` skips the remainder of the loop and continues executing from the next round of the loop. Let’s practice:

``````>>> for num in range(0, 100):
...     print(f"Testing number {num}")
...     if num == 3:
...         print("Found number 3!")
...         break
...     print("Not yet...")
...
``````

Notice that “Not yet…” doesn’t get printed for number 3, because we `break` out of the loop first. Let’s try a `continue`:

``````>>> for num in range(0, 100):
...     print(f"Testing number {num}")
...     if num < 3:
...         continue
...     elif num == 5:
...         print("Found number 5!")
...         break
...     print("Not yet...")
...
``````

Notice that “Not yet…” doesn’t get printed at all until the number is 3, because the `continue` short-circuits the loop back to the beginning. Then we `break` when we hit 5.

You can also use the `return` keyword to break out of a loop within a function, while optionally returning a value.

``````>>> def is_number_in_list(number_to_check, list_to_search):
...     for num in list_to_search:
...         print(f"Checking {num}...")
...         if num == number_to_check:
...             return True
...     return False
>>> my_list = [1, 2, 3, 4, 5]
>>> is_number_in_list(27, my_list)
>>> is_number_in_list(2, my_list)
``````

Notice that our function `is_number_in_list` checks all the numbers in `my_list` on the first run, but on the next run, stops immediately when it hits 3 and returns `True`.

Here's what you should have seen in your REPL:

## `while` loop

Instead of looping over a sequence, `while` loops continue looping while a certain condition is met (or not met). The condition is checked at the beginning every iteration.

``````>>> counter = 0
>>> while counter < 3:
...     print(f"Counter = {counter}")
...     counter += 1
``````

Notice that the loop ends once `counter` 3, and the remainder of the loop is bypassed. You can also loop forever by using `while True` or `while False`, but you should make sure you have solid `break` conditions, or your program will just loop forever (unless that’s what you want).

``````>>> counter = 0
>>> while True:
...     print(f"Counter = {counter}")
...     if counter == 3:
...         break
...     counter += 1
``````
Here's what you should have seen in your REPL:

## Nested Loops

Nesting loops is often necessary and sometimes tricky. The `break` keyword will only get you out of whichever loop you’re `break`ing. The only way to exit all loops is with multiple `break` statements (at each level), or the `return` keyword (inside a function). For example:

``````names = ["Rose", "Max", "Nina"]
target_letter = 'x'
found = False

for name in names:
for char in name:
if char == target_letter:
found = True
break

if found:
print(f"Found {name} with letter: {target_letter}")
break
``````

Or:

``````>>> for x in range(0, 5):
...     for y in range(0, 5):
...         print(f"x = {x}, y = {y}")
...         if y == 2:
...             break
...
``````

Notice how the inner `y` loop never gets above 2, whereas the outer `x` loop continues until the end of its range.

Here's what you should have seen in your REPL: