diff --git a/_quarto.yml b/_quarto.yml
index dd136db1..38e9ce16 100644
--- a/_quarto.yml
+++ b/_quarto.yml
@@ -182,6 +182,28 @@ website:
- text: ' 6.1. Exercises'
href: modules/module5/module5-20-function_questions.qmd
- href: modules/module5/module5-23-what_did_we_just_learn.qmd
+ - section: "**Module 6: Functions Fundamentals and Best Practices**"
+ contents:
+ - href: modules/module6/module6-00-module_learning_outcomes.qmd
+ - href: modules/module6/module6-01-dry_revisited_and_function_fundamentals.qmd
+ - text: ' 1.1. Exercises'
+ href: modules/module6/module6-02-questions_on_scoping.qmd
+ - href: modules/module6/module6-05-default_arguments.qmd
+ - text: ' 2.1. Exercises'
+ href: modules/module6/module6-06-will_it_output.qmd
+ - href: modules/module6/module6-09-function_docstrings.qmd
+ - text: ' 3.1. Exercises'
+ href: modules/module6/module6-10-docstring_questions.qmd
+ - href: modules/module6/module6-13-defensive_programming_using_exceptions.qmd
+ - text: ' 4.1. Exercises'
+ href: modules/module6/module6-14-exceptions.qmd
+ - href: modules/module6/module6-17-unit_tests.qmd
+ - text: ' 5.1. Exercises'
+ href: modules/module6/module6-18-assert_questions.qmd
+ - href: modules/module6/module6-22-good_function_design_choices.qmd
+ - text: ' 6.1. Exercises'
+ href: modules/module6/module6-23-function_design_questions.qmd
+ - href: modules/module6/module6-26-what_did_we_just_learn.qmd
# Since we are declaring options for two formats here (html and revealjs)
# each qmd file needs to include a yaml block including which format to use for that file.
diff --git a/modules/module6/module6-00-module_learning_outcomes.qmd b/modules/module6/module6-00-module_learning_outcomes.qmd
new file mode 100644
index 00000000..3b445db7
--- /dev/null
+++ b/modules/module6/module6-00-module_learning_outcomes.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 0. Module Learning Outcomes
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-01-dry_revisited_and_function_fundamentals.qmd b/modules/module6/module6-01-dry_revisited_and_function_fundamentals.qmd
new file mode 100644
index 00000000..3688dc3a
--- /dev/null
+++ b/modules/module6/module6-01-dry_revisited_and_function_fundamentals.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 1. DRY Revisited and Function Fundamentals
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-02-questions_on_scoping.qmd b/modules/module6/module6-02-questions_on_scoping.qmd
new file mode 100644
index 00000000..63312482
--- /dev/null
+++ b/modules/module6/module6-02-questions_on_scoping.qmd
@@ -0,0 +1,254 @@
+---
+format: live-html
+---
+
+
+
+# 1.1. Exercises
+
+## Questions on Scoping
+
+```python
+toy = "ball"
+
+def playtime():
+ toy = "truck"
+ return toy
+
+toy
+```
+
+
+
+
+```python
+toy = "ball"
+
+def playtime():
+ toy = "truck"
+ return toy
+
+playtime()
+```
+
+
+
+
+```python
+def playtime():
+ toy = "truck"
+ return toy
+
+toy
+```
+
+
+
+
+
+## Side Effects
+
+
+
+
+A.
+
+```python
+toy = "ball"
+
+def playtime():
+ toy = "truck"
+ print(toy)
+
+playtime()
+```
+
+B.
+
+```python
+
+toy = "ball"
+
+def playtime():
+ toy = "truck"
+ return toy
+
+playtime()
+```
+
+
+
+
+
+
+
+
+## Writing Functions Without Side Effects
+
+**Instructions:**
+Running a coding exercise for the first time could take a bit of time for everything to load. Be patient, it could take a few minutes.
+
+**When you see `____` in a coding exercise, replace it with what you assume to be the correct code. Run it and see if you obtain the desired output. Submit your code to validate if you were correct.**
+
+_**Make sure you remove the hash (`#`) symbol in the coding portions of this question. We have commented them so that the line won't execute and you can test your code after each step.**_
+
+The function `kg_to_lb()` is used to convert a list of elements with kg units into a list of elements with lbs units. Unfortunate this function includes a side effect that edits one of the global variables.
+
+```{pyodide}
+weight_kg = [90, 41, 65, 76, 54, 88]
+weight_lb = list()
+
+def kg_to_lb(weight_list):
+ conversion = 2.20462
+ for kg in weight_kg:
+ weight_lb.append(kg * conversion)
+ return
+
+kg_to_lb(weight_kg)
+weight_lb
+```
+
+**Tasks:**
+
+- Write a new function named `better_kg_to_lb` that no longer contains a side effect.
+- Test your new function on the `weight_kg` list and save the results in an object named `weight_lb_again`.
+
+```{pyodide}
+#| setup: true
+#| exercise: writing_functions_without_side_effects
+import pandas as pd
+```
+
+```{pyodide}
+#| exercise: writing_functions_without_side_effects
+weight_kg = [90, 41, 65, 76, 54, 88]
+
+# rewrite the code and function above so that it does not have any side effects
+____ ____(____):
+ ____
+ ____
+ ____
+ ____
+
+# Test your new function on weight_kg and name the new object weight_lb_again
+____ = ____
+____
+```
+
+```{pyodide}
+#| exercise: writing_functions_without_side_effects
+#| check: true
+from src.utils import print_correct_msg
+
+assert isinstance(result, list), "Your function should return a list."
+assert len(result) == 6, "Your function output is incorrect. Are you getting rid of side effects?"
+assert round(sum(result), 2) == 912.71, "Your function output is incorrect. Check your calculation."
+print_correct_msg()
+```
+
+:::: { .hint exercise="writing_functions_without_side_effects"}
+::: { .callout-note collapse="false"}
+
+## Hint 1
+
+- Are you putting `weight_lb` inside the function now?
+- Are you returning `weight_lb`?
+
+:::
+::::
+
+:::: { .solution exercise="writing_functions_without_side_effects" }
+::: { .callout-tip collapse="false"}
+
+## Fully worked solution:
+
+```{pyodide}
+weight_kg = [90, 41, 65, 76, 54, 88]
+
+# rewrite the code and function above so that it does not have any side effects
+def better_kg_to_lb(weight_list):
+ weight_lb = list()
+ conversion = 2.20462
+
+ for kg in weight_list:
+ weight_lb.append(kg * conversion)
+ return weight_lb
+
+# Test your new function on weight_kg and name the new object weight_lb_again
+weight_lb_again = better_kg_to_lb(weight_kg)
+weight_lb_again
+```
+
+:::
+::::
\ No newline at end of file
diff --git a/modules/module6/module6-05-default_arguments.qmd b/modules/module6/module6-05-default_arguments.qmd
new file mode 100644
index 00000000..e81ba88d
--- /dev/null
+++ b/modules/module6/module6-05-default_arguments.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 2. Default Arguments
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-06-will_it_output.qmd b/modules/module6/module6-06-will_it_output.qmd
new file mode 100644
index 00000000..b8e49b53
--- /dev/null
+++ b/modules/module6/module6-06-will_it_output.qmd
@@ -0,0 +1,208 @@
+---
+format: live-html
+---
+
+
+
+# 2.1. Exercises
+
+## Questions on Scoping
+
+```python
+def name_initials(first_name, last_name, year_of_birth = None):
+ if year_of_birth is not None:
+ initials = first_name[0].upper() + middle_name[0].upper() + str(year_of_birth)
+ else:
+ initials = first_name[0].upper() + last_name[0].upper()
+ return initials
+```
+
+
+
+
+## Default Arguments
+
+Given the function below:
+
+```python
+def employee_wage(employee_id, position, experience = 3):
+ if position == "doctor":
+ wage = 150000
+ elif position == "teacher":
+ wage = 60000
+ elif position == "lawyer":
+ wage = 100000
+ elif position == "server":
+ wage = 50000
+ else:
+ wage = 70000
+ return wage * (1 + (0.1 * experience))
+```
+
+
+
+
+```python
+menu_item_id = 42
+menu_item_name = "Green Curry"
+spice_level = "Medium"
+meat = "Tofu"
+rice = True
+
+thai_food(menu_item_id, menu_item_name, spice_level, meat, rice)
+```
+
+
+
+
+
+## Default Argument Practice
+
+**Instructions:**
+Running a coding exercise for the first time could take a bit of time for everything to load. Be patient, it could take a few minutes.
+
+**When you see `____` in a coding exercise, replace it with what you assume to be the correct code. Run it and see if you obtain the desired output. Submit your code to validate if you were correct.**
+
+_**Make sure you remove the hash (`#`) symbol in the coding portions of this question. We have commented them so that the line won't execute and you can test your code after each step.**_
+
+**Weight** and **mass** are 2 very different measurements, although they are used interchangeably in everyday conversations.
+**Mass** is defined by NASA as the amount of matter in an object, whereas, **weight** is defined as the vertical force exerted by a mass as a result of gravity (with units of Newtons).
+The function `earth_weight()` converts an object's mass to Weight by multiplying it by the gravitational force acting on it. On Earth, the gravitational force is measured as 9.8 m/s2.
+
+```{pyodide}
+def earth_weight(mass):
+ g = 9.8
+ weight = mass * g
+ return weight
+```
+
+We want to make a more versatile function by having the ability to calculate the Weight of any object on any particular planet and not just Earth. Redefine the function `earth_weight()` to take an argument with a default gravitational force of 9.8.
+
+**Tasks:**
+
+- Create a new function named `mass_to_weight` and give it an additional argument named `g,` which has a default value of 9.8.
+- Test your new function by converting the mass of 76 kg to weight on Earth and save the results in an object named `earth_weight`.
+- Test your function again but this time, calculate the weight of the 76 kg object on the moon using a gravitational force of 1.62 m/s2 and save your function call to an object named `moon_weight`.
+
+```{pyodide}
+#| setup: true
+#| exercise: default_argument_practice
+import pandas as pd
+```
+
+```{pyodide}
+#| exercise: default_argument_practice
+# Create a new function named mass_to_weight() from earth_weight()
+# Give it an additional argument named g which has a default value of 9.8
+____ ____(____):
+ ____
+ ____
+
+# Test your new function by converting the mass of 76 kg to weight on Earth
+# Save the results in an object named earth_weight
+____ = ____
+
+# Test your function again but this time calculate the weight
+# of the 76 kg object on the moon using a gravitational force of 1.62 m/s^2
+# Save your function call to an object named moon_weight
+# ____ = ____
+
+result = {
+ "earth_weight": earth_weight,
+ "moon_weight": moon_weight
+}
+result
+```
+
+```{pyodide}
+#| exercise: default_argument_practice
+#| check: true
+from src.utils import print_correct_msg
+
+assert isinstance(result, dict), "Your result should be a dict."
+assert result["earth_weight"] == 76*9.8, "Your function's output is incorrect. Are you converting correctly?"
+assert result["moon_weight"] == 76*1.62, "Your function's output is incorrect. Are you converting correctly?"
+print_correct_msg()
+```
+
+:::: { .hint exercise="default_argument_practice"}
+::: { .callout-note collapse="false"}
+
+## Hint 1
+
+- Are you putting `g=9.8` inside the function named `mass_to_weight`?
+- Are you calling `mass_to_weight(76)` and saving it in an object named `earth_weight`?
+- Are you calling `mass_to_weight(76, 1.62)` and saving it in an object named `moon_weight`?
+
+:::
+::::
+
+:::: { .solution exercise="default_argument_practice" }
+::: { .callout-tip collapse="false"}
+
+## Fully worked solution:
+
+```{pyodide}
+# Create a new function named mass_to_weight() from earth_weight()
+# Give it an additional argument named g which has a default value of 9.8
+def mass_to_weight(mass, g=9.8):
+ weight = mass * g
+ return weight
+
+# Test your new function by converting the mass of 76 kg to weight on Earth
+# Save the results in an object named earth_weight
+earth_weight = mass_to_weight(76)
+
+# Test your function again but this time calculate the weight
+# of the 76 kg object on the moon using a gravitational force of 1.62 m/s^2
+# Save your function call to an object named moon_weight
+moon_weight = mass_to_weight(76, 1.62)
+
+result = {
+ "earth_weight": earth_weight,
+ "moon_weight": moon_weight
+}
+result
+```
+
+:::
+::::
\ No newline at end of file
diff --git a/modules/module6/module6-09-function_docstrings.qmd b/modules/module6/module6-09-function_docstrings.qmd
new file mode 100644
index 00000000..804295e1
--- /dev/null
+++ b/modules/module6/module6-09-function_docstrings.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 3. Function Docstrings
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-10-docstring_questions.qmd b/modules/module6/module6-10-docstring_questions.qmd
new file mode 100644
index 00000000..28d10e18
--- /dev/null
+++ b/modules/module6/module6-10-docstring_questions.qmd
@@ -0,0 +1,339 @@
+---
+format: live-html
+---
+
+
+
+# 3.1. Exercises
+
+## Docstring Questions
+
+```python
+def factor_of_10(number):
+ """
+ Takes a number and determines if it is a factor of 10
+ Parameters
+ ----------
+ number : int
+ the value to check
+
+ Returns
+ -------
+ bool
+ Returns True if numbers are a multiple of 10 and False otherwise
+
+ Examples
+ --------
+ >>> factor_of_10(72)
+ False
+ """
+
+ if number % 10 == 0:
+ factor = True
+ else:
+ factor = False
+
+ return factor
+```
+
+
+
+
+```python
+def add_stars(name):
+ """
+ This will return your input string between a pair of stars.
+ Parameters
+ ----------
+ name: str
+ a sentence or word
+
+ Returns
+ -------
+ str
+ The initial string beginning and ending with a pair of stars
+
+ Examples
+ --------
+ >>> add_stars('Good Job')
+ 'Good Job'
+ """
+
+ name = '**' + name + '**'
+ return
+```
+
+
+
+
+
+## Which Docstring is Most Appropriate?
+
+Given the function below:
+
+```python
+def acronym_it(sentence):
+ words = sentence.split()
+ first_letters = [word[0].upper() for word in words]
+ acronym = "".join(first_letters)
+ return acronym
+```
+
+A.
+
+```python
+"""
+A function that converts a string into an acronym of capital
+letters
+
+Input
+------
+str : sentence
+ The string to obtain the first letters from
+
+Output
+-------
+str
+ A string of the first letters of each word in an uppercase format
+
+Sample
+-------
+>>> acronym_it("Let's make this an acronym")
+"LMTAA"
+"""
+
+```
+
+B.
+
+```python
+"""
+A function that converts a string into an acronym of capital
+letters
+
+Input
+------
+some_words : str
+ The string to obtain the first letters from
+
+Output
+-------
+list
+ A list of the first letters of each word from the input string
+
+Example
+-------
+>>> acronym_it("Let's make this an acronym")
+"LMTAA"
+"""
+```
+
+
+C.
+
+```python
+"""
+A function that converts a string into an acronym of capital
+letters
+
+Parameters
+----------
+sentence : str
+ The string to obtain the first letters from
+
+Returns
+-------
+str
+ a string of just first letters in an uppercase format
+
+Example
+-------
+>>> acronym_it("Let's make this an acronym")
+"LMTAA"
+"""
+```
+
+
+D.
+
+```python
+"""
+A function that converts a string into an acronym of capital
+letters
+
+
+Returns
+-------
+list :
+ A list of the first letters of each word from the input string
+
+Parameters
+----------
+str : sentence
+ The string to obtain the first letters from
+
+Example
+-------
+>>> acronym_it("Let's make this an acronym")
+"LMTAA"
+"""
+```
+
+
+
+
+
+## Practice Writing a Docstring
+
+**Instructions:**
+Running a coding exercise for the first time could take a bit of time for everything to load. Be patient, it could take a few minutes.
+
+**When you see `____` in a coding exercise, replace it with what you assume to be the correct code. Run it and see if you obtain the desired output. Submit your code to validate if you were correct.**
+
+_**Make sure you remove the hash (`#`) symbol in the coding portions of this question. We have commented them so that the line won't execute and you can test your code after each step.**_
+
+In module 5, we wrote a function that returns the BMI given a person's weight and height. Let's write a docstring for it now!
+
+```python
+def bmi_calculator(height, weight):
+ return (weight / (height ** 2)) * 702
+```
+
+**Tasks:**
+
+- Write a NumPy style docstring for the function provided.
+- For this question, we want you to write your docstring between 3 single quotations `'''` instead of the normal double quotations `"""`. This will allow us to test the solution provided.
+- Make sure to include a brief description, parameters, return, and example sections.
+- View the documentation of the function.
+
+```{pyodide}
+#| setup: true
+#| exercise: practice_writing_a_docstring
+import pandas as pd
+```
+
+```{pyodide}
+#| exercise: practice_writing_a_docstring
+# Make a docstring for the function below:
+def bmi_calculator(height, weight):
+ '''
+
+ Write your docstring here
+
+ '''
+
+ return (weight / (height ** 2)) * 702
+
+
+# View the documentation
+____
+
+# For checking purposes, do not use print()
+bmi_calculator.__doc__
+```
+
+```{pyodide}
+#| exercise: practice_writing_a_docstring
+#| check: true
+from src.utils import print_correct_msg
+
+assert isinstance(result, str), "Your result should be a string."
+assert "Parameters" in result, "Make sure your docstring includes a 'Parameter' section"
+assert "height" in result.lower(), "Make sure you are describing the 'height' argument in the docstring"
+assert "weight" in result.lower(), "Make sure you are describing the 'weight' argument in the docstring"
+assert "Returns" in result, "Make sure your docstring includes a 'Returns' section"
+assert "Examples" in result, "Make sure your docstring includes an 'Examples' section"
+print_correct_msg()
+```
+
+:::: { .hint exercise="practice_writing_a_docstring"}
+::: { .callout-note collapse="false"}
+
+## Hint 1
+
+- Are you using `'''` to contain your docstring?
+- Are you including all the sections?
+- Are you getting the documentation of the docstring using `?bmi_calculator`
+
+:::
+::::
+
+:::: { .solution exercise="practice_writing_a_docstring" }
+::: { .callout-tip collapse="false"}
+
+## Fully worked solution:
+
+```{pyodide}
+# Make a docstring for the function below:
+
+def bmi_calculator(height, weight):
+ '''
+ Calculates and returns the Body Mass Index of an individual
+
+ Parameters
+ ----------
+ height : float
+ The height of an individual in inches
+ weight : float
+ The weight of an individual in lbs
+
+ Returns
+ -------
+ float
+ The Body Mass Index
+
+ Examples
+ --------
+ >>> bmi_calculator(62, 105)
+ 19.175338189386057
+ '''
+ return (weight/(height**2)) * 702
+
+# View the documentation
+print(bmi_calculator.__doc__)
+
+# for checking purposes, please uncomment the following line
+# bmi_calculator.__doc__
+```
+
+:::
+::::
\ No newline at end of file
diff --git a/modules/module6/module6-13-defensive_programming_using_exceptions.qmd b/modules/module6/module6-13-defensive_programming_using_exceptions.qmd
new file mode 100644
index 00000000..a7124700
--- /dev/null
+++ b/modules/module6/module6-13-defensive_programming_using_exceptions.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 4. Defensive Programming using Exceptions
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-14-exceptions.qmd b/modules/module6/module6-14-exceptions.qmd
new file mode 100644
index 00000000..c319e720
--- /dev/null
+++ b/modules/module6/module6-14-exceptions.qmd
@@ -0,0 +1,240 @@
+---
+format: live-html
+---
+
+
+
+# 4.1. Exercises
+
+## Exceptions
+
+
+
+
+
+
+
+## Documenting Exceptions
+
+
+
+
+A.
+
+```python
+def factor_of_10(number):
+ """
+ Takes a number and determines if it is a factor of 10
+ Parameters
+ ----------
+ number : int
+ the value to check
+
+ Returns
+ -------
+ bool
+ Returns True if number is a multiple of 10 and False otherwise
+
+ Raises
+ ------
+ TypeError
+ If the input argument number is not of type int
+
+ Examples
+ --------
+ >>> factor_of_10(72)
+ False
+ """
+ if not isinstance(number, int):
+ raise TypeError("the input value of number is not of type int")
+
+ if number % 10 == 0:
+ factor = True
+ else:
+ factor = False
+
+ return factor
+```
+
+
+B.
+
+```python
+def factor_of_10(number):
+ """
+ Takes a number and determines if it is a factor of 10
+ Parameters
+ ----------
+ number : int
+ the value to check
+
+ Returns
+ -------
+ bool
+ Returns True if number is a multiple of 10 and False otherwise
+
+ Exceptions
+ ------
+ TypeError
+ If the input argument number is not of type int
+
+ Examples
+ --------
+ >>> factor_of_10(72)
+ False
+ """
+ if not isinstance(number, int):
+ raise TypeError("the input value of number is not of type int")
+
+ if number % 10 == 0:
+ factor = True
+ else:
+ factor = False
+
+ return factor
+```
+
+
+## Raising Exceptions
+
+**Instructions:**
+Running a coding exercise for the first time could take a bit of time for everything to load. Be patient, it could take a few minutes.
+
+**When you see `____` in a coding exercise, replace it with what you assume to be the correct code. Run it and see if you obtain the desired output. Submit your code to validate if you were correct.**
+
+_**Make sure you remove the hash (`#`) symbol in the coding portions of this question. We have commented them so that the line won't execute and you can test your code after each step.**_
+
+Let's build on the BMI function we made in module 5. This time we want to raise 2 exceptions.
+
+
+```python
+def bmi_calculator(height, weight):
+ return (weight / (height ** 2)) * 702
+```
+
+**Tasks:**
+
+- Write an exception that checks if `height` is of type `float`.
+- Write a second exception that raises an error if weight is 0 or less.
+- Test your function with the values given in variable `tall` and `mass`.
+
+```{pyodide}
+#| setup: true
+#| exercise: raising_exceptions
+import pandas as pd
+```
+
+```{pyodide}
+#| exercise: raising_exceptions
+# Add an exception to the function below that checks if height is of type float and
+# an exception that raises an error if weight is 0 or less.
+def bmi_calculator(height, weight):
+ ____
+ ____
+
+ ____
+ ____
+
+ return (weight/(height**2)) * 702
+
+
+# Test your function with the values below
+tall = 193
+mass = 170.2
+
+____
+
+# For checking purposes:
+bmi_calculator.__code__.co_names
+```
+
+```{pyodide}
+#| exercise: raising_exceptions
+#| check: true
+from src.utils import print_correct_msg
+
+assert isinstance(result, tuple), "Your result should be a tuple."
+assert "isinstance" in result or "type" in result, "Are you checking the type of `height`?"
+assert "float" in result, "Are you checking the type of `height`?"
+assert "TypeError" in result, "Make sure you are raising a 'TypeError' for an incorrect data type."
+assert 'Exception' in result, "Make sure you are raising a 'Exception' for an incorrect data value."
+print_correct_msg()
+```
+
+:::: { .hint exercise="raising_exceptions"}
+::: { .callout-note collapse="false"}
+
+## Hint 1
+
+- Are you using `TypeError` and `Exception` respectively for the exception messages?
+- Are you checking the `height` type with `if type(height) is not float:`?
+- Are you checking if weight is greater than 0 with `if weight <= 0:`?
+
+:::
+::::
+
+:::: { .solution exercise="raising_exceptions" }
+::: { .callout-tip collapse="false"}
+
+## Fully worked solution:
+
+```{pyodide}
+# Add an exception to the function below that checks if height is of type float and
+# an exception that raises an error if weight is 0 or less.
+def bmi_calculator(height, weight):
+ if type(height) is not float:
+ raise TypeError("Sorry, but you are not using a float for input variable")
+
+ if weight <= 0:
+ raise Exception("Weight must be a positive value")
+
+ return (weight/(height**2)) * 702
+
+
+# Test your function with the values below
+tall = 193
+mass = 170.2
+
+# For checking purposes:
+bmi_calculator.__code__.co_names
+```
+
+:::
+::::
\ No newline at end of file
diff --git a/modules/module6/module6-17-unit_tests.qmd b/modules/module6/module6-17-unit_tests.qmd
new file mode 100644
index 00000000..41c39438
--- /dev/null
+++ b/modules/module6/module6-17-unit_tests.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 5. Unit tests
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-18-assert_questions.qmd b/modules/module6/module6-18-assert_questions.qmd
new file mode 100644
index 00000000..d960cee1
--- /dev/null
+++ b/modules/module6/module6-18-assert_questions.qmd
@@ -0,0 +1,184 @@
+---
+format: live-html
+---
+
+
+
+# 5.1. Exercises
+
+## Assert Questions
+
+
+
+
+## Unit Tests Questions
+
+```python
+def acronym_it(sentence):
+ words = sentence.split()
+ first_letters = [word[0].upper() for word in words]
+ acronym = "".join(first_letters)
+ return acronym
+```
+
+
+
+
+
+
+
+## Unit Tests and Test-Driven Development Questions
+
+
+
+
+
+
+
+
+
+
+
+## Writing Tests
+
+**Instructions:**
+Running a coding exercise for the first time could take a bit of time for everything to load. Be patient, it could take a few minutes.
+
+**When you see `____` in a coding exercise, replace it with what you assume to be the correct code. Run it and see if you obtain the desired output. Submit your code to validate if you were correct.**
+
+_**Make sure you remove the hash (`#`) symbol in the coding portions of this question. We have commented them so that the line won't execute and you can test your code after each step.**_
+
+Given our BMI function from the previous few questions, let's write some unit tests.
+
+**Tasks:**
+
+- Write at least 4 unit tests and check that at least 2 of them are testing edge cases.
+- For this exercsie, use single quotes `''` instead of double quotes `""` for the `assert` messages.
+
+```{pyodide}
+#| exercise: writing_tests
+def bmi_calculator(height, weight):
+ return (weight/(height**2)) * 702
+
+
+# Write 4 unit tests and check that at least 2 of them are testing edge cases
+____
+____
+____
+____
+```
+
+```{pyodide}
+#| exercise: writing_tests
+#| check: true
+assert False, "No tests available. Please check the solution."
+```
+
+:::: { .hint exercise="writing_tests"}
+::: { .callout-note collapse="false"}
+
+## Hint 1
+
+- Are you using `Assert` statements?
+- Are you checking that they equal a correct value?
+
+:::
+::::
+
+:::: { .solution exercise="writing_tests" }
+::: { .callout-tip collapse="false"}
+
+## Fully worked solution:
+
+```{pyodide}
+def bmi_calculator(height, weight):
+ return (weight/(height**2)) * 702
+
+# Write 4 unit tests and check that at least 2 of them are testing edge cases
+# There are many different tests that could have been written for this question.
+# Here are a few examples.
+assert bmi_calculator(1, 1) == 702.0, 'Input arguments giving incorrect output'
+assert bmi_calculator(100, 10000) == 702.0, 'Input arguments giving incorrect output'
+assert bmi_calculator(1, 0) == 0, 'Input arguments giving incorrect output'
+assert type(bmi_calculator(12,8)) == float, 'Output result type is incorrect'
+assert bmi_calculator(20, 400) == 702.0, 'Input arguments giving incorrect output'
+assert bmi_calculator(20, 800) == 1404.0, 'Input arguments giving incorrect output'
+```
+
+:::
+::::
\ No newline at end of file
diff --git a/modules/module6/module6-22-good_function_design_choices.qmd b/modules/module6/module6-22-good_function_design_choices.qmd
new file mode 100644
index 00000000..6dd8451f
--- /dev/null
+++ b/modules/module6/module6-22-good_function_design_choices.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 6. Good Function Design Choices
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/module6-23-function_design_questions.qmd b/modules/module6/module6-23-function_design_questions.qmd
new file mode 100644
index 00000000..d9777aa5
--- /dev/null
+++ b/modules/module6/module6-23-function_design_questions.qmd
@@ -0,0 +1,211 @@
+---
+format: live-html
+---
+
+
+
+# 6.1. Exercises
+
+## Function Design Questions
+
+```python
+def give_me_facts(myinfo):
+ max_val = max(myinfo)
+ min_val = min(myinfo)
+ range_val = max_val - min_val
+ return max_val, min_val, range_val
+```
+
+
+
+
+## Improve it!
+
+```python
+
+def count_the_sevens(mylist):
+ number_of_occurances = mylist.count(7)
+ return number_of_occurances
+```
+
+
+
+
+## Function Design
+
+**Instructions:**
+Running a coding exercise for the first time could take a bit of time for everything to load. Be patient, it could take a few minutes.
+
+**When you see `____` in a coding exercise, replace it with what you assume to be the correct code. Run it and see if you obtain the desired output. Submit your code to validate if you were correct.**
+
+_**Make sure you remove the hash (`#`) symbol in the coding portions of this question. We have commented them so that the line won't execute and you can test your code after each step.**_
+
+Given the function below, improve it so that it follows good function design principles.
+
+```{pyodide}
+def wage_increase(group):
+ '''
+ Calculates a new wage given a 10% increase for each element in a list and return
+ a list of containing the new salaries and a list of the raise increases
+
+ Parameters
+ ----------
+ group : list
+ a list containing a group of people's wages
+
+ Returns
+ -------
+ new_salary: list
+ a list containing new salaries after each undergoing a 10% wage increase
+ raise_increase: list
+ a list containing the absolute wage increase each element underwent.
+
+ Examples
+ --------
+ >>> wage_increase([20000, 76000, 110000, 88000])
+ '''
+ new_salary = list()
+ raise_increase = list()
+
+ for salary in group:
+ new_salary.append(round(salary * 1.10))
+ raise_increase.append(round(salary * 0.10))
+
+ return new_salary, raise_increase
+```
+
+**Tasks:**
+
+- Given the function above, improve it using to the new function `new_wage()`. We have provided you with the function stub and the docstring to guide you.
+- Make sure it follows good function design practices by not looping over a function, avoiding hard-coding and not returning multiple values.
+- Test your new function on a person with a salary of $84000 and an expected raise of 12%.
+- Save this in an object named `person1_new_wage`.
+
+```{pyodide}
+#| setup: true
+#| exercise: function_design
+import pandas as pd
+```
+
+```{pyodide}
+#| exercise: function_design
+# Given the function above, improve it so that it follows best function design practices
+# Name your new function new_wage()
+def new_wage(salary, percent_raise):
+ '''
+ Calculates a new wage given a percentage
+
+ Parameters
+ ----------
+ salary : int or float
+ a salary expected to change
+ percent_raise : int or float
+ an expect percent which the salary will change
+
+ Returns
+ -------
+ float
+ the new salary after undergoing the pay change percentage
+
+ Examples
+ --------
+ >>> new_wage(30000, 30)
+ 39000.0
+ '''
+
+ return ____
+
+# Check your new function on a person with a salary of $84000 and an expected raise of 12%
+# Save this in an object named person1_new_wage
+person1_new_wage = ____
+person1_new_wage
+```
+
+```{pyodide}
+#| exercise: function_design
+#| check: true
+from src.utils import print_correct_msg
+
+assert result == 84000 * (1 + (0.01 * 12)), "Your function output is incorrect. Please revise and try again."
+print_correct_msg()
+```
+
+:::: { .hint exercise="function_design"}
+::: { .callout-note collapse="false"}
+
+## Hint 1
+
+- Are you removing the hardcoded values in the function?
+- Are removing the return of 2 variables?
+- Are you removing the loop from inside the function?
+- Are you multiplying the `salary` by `(1 + (0.01 * percent_raise)`?
+
+:::
+::::
+
+:::: { .solution exercise="function_design" }
+::: { .callout-tip collapse="false"}
+
+## Fully worked solution:
+
+```{pyodide}
+# Given the function above, improve it so that it follows best function design practices
+# Name your new function new_wage()
+def new_wage(salary, percent_raise):
+ '''
+ Calculates a new wage given a percentage
+
+ Parameters
+ ----------
+ salary : int or float
+ a salary expected to change
+ percent_raise : int or float
+ an expect percent which the salary will change
+
+ Returns
+ -------
+ float
+ the new salary after undergoing the pay change percentage
+
+ Examples
+ --------
+ >>> new_wage(30000, 30)
+ 39000.0
+ '''
+
+ return salary * (1 + (0.01 * percent_raise))
+
+# Check your new function on a person with a salary of $84000 and an expected raise of 12%
+# Save this in an object named person1_new_wage
+person1_new_wage = new_wage(84000, 12)
+person1_new_wage
+```
+
+:::
+::::
\ No newline at end of file
diff --git a/modules/module6/module6-26-what_did_we_just_learn.qmd b/modules/module6/module6-26-what_did_we_just_learn.qmd
new file mode 100644
index 00000000..294bb5c4
--- /dev/null
+++ b/modules/module6/module6-26-what_did_we_just_learn.qmd
@@ -0,0 +1,29 @@
+---
+format:
+ html:
+ page-layout: full
+---
+
+# 7. What Did We Just Learn?
+
+::: {.panel-tabset .nav-pills}
+
+## Video
+
+
+
+## Slides
+
+
+
+:::
diff --git a/modules/module6/slides/module6_00.qmd b/modules/module6/slides/module6_00.qmd
new file mode 100644
index 00000000..36800d96
--- /dev/null
+++ b/modules/module6/slides/module6_00.qmd
@@ -0,0 +1,29 @@
+---
+format: revealjs
+title: Module Learning Outcomes
+title-slide-attributes:
+ data-notes: |
+ In this module you will expand your knowledge on functions and the best practices when developing functions and code.
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+## Module Learning Outcomes
+
+By the end of the module, students are expected to:
+
+- Evaluate the readability, complexity and performance of a function.
+- Write docstrings for functions following the NumPy/SciPy format.
+- Write comments within a function to improve readability.
+- Write and design functions with default arguments.
+- Explain the importance of scoping and environments in Python as they relate to functions.
+- Formulate test cases to prove a function design specification.
+- Use `assert` statements to formulate a test case to prove a function design specification.
+- Use test-driven development principles to define a function that accepts parameters, returns values and passes all tests.
+- Handle errors gracefully via exception handling.
+
+
+# Let's Start!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_01.qmd b/modules/module6/slides/module6_01.qmd
new file mode 100644
index 00000000..9f9f3e58
--- /dev/null
+++ b/modules/module6/slides/module6_01.qmd
@@ -0,0 +1,628 @@
+---
+format: revealjs
+title: DRY Revisited and Function Fundamentals
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+
+
+```{python}
+numbers = [2, 3, 5]
+squared = list()
+for number in numbers:
+ squared.append(number ** 2)
+squared
+```
+
+
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```{python}
+squares_a_list(numbers)
+```
+
+
+:::{.notes}
+In the last module, we were introduced to the DRY principle and how creating functions helps comply with it.
+
+Let's do a little bit of a recap.
+
+**DRY** stands for Don't Repeat Yourself.
+
+We can avoid writing repetitive code by creating a function that takes in arguments, performs some operations, and returns the results.
+
+The example in Module 5 converted code that creates a list of squared elements from an existing list of numbers into a function.
+:::
+
+---
+
+```{python}
+larger_numbers = [5, 44, 55, 23, 11]
+promoted_numbers = [73, 84, 95]
+executive_numbers = [100, 121, 250, 103, 183, 222, 214]
+```
+
+
+
+```{python}
+squares_a_list(larger_numbers)
+```
+
+
+
+```{python}
+squares_a_list(promoted_numbers)
+```
+
+
+
+```{python}
+squares_a_list(executive_numbers)
+```
+
+
+:::{.notes}
+This function gave us the ability to do the same operation for multiple lists without having to rewrite any code and just calling the function.
+:::
+
+---
+
+## Scoping
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ print(new_squared_list)
+ return new_squared_list
+```
+
+
+
+```{python}
+squares_a_list(numbers)
+```
+
+
+
+```python
+new_squared_list
+```
+
+```out
+NameError: name 'new_squared_list' is not defined
+
+Detailed traceback:
+ File "", line 1, in
+```
+
+
+:::{.notes}
+It's important to know what exactly is going on inside and outside of a function.
+
+In our function `squares_a_list()` we saw that we created a variable named `new_squared_list`.
+
+We can print this variable and watch all the elements be appended to it as we loop through the input list.
+
+But what happens if we try and print this variable outside of the function?
+
+Yikes! Where did `new_squared_list` go?
+
+It doesn't seem to exist! That's not entirely true.
+
+In Python, `new_squared_list` is something we call a ***local variable***.
+
+Local variables are any objects that have been created within a function and only exist inside the function where they are made.
+
+Code within a function is described as a **local environment**.
+
+Since we called `new_squared_list` outside of the function's body, Python fails to recognize it.
+:::
+
+---
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ print(new_squared_list)
+ return new_squared_list
+```
+
+
+
+```{python}
+a_new_variable = "Peek-a-boo"
+a_new_variable
+```
+
+
+:::{.notes}
+Let's compare that with the variable `a_new_variable`.
+
+`a_new_variable` is created outside of a function in what we call our ***global environment***, and therefore Python recognizes it as a ***global variable***.
+:::
+
+---
+
+## Global and Local Variables
+
+```{python}
+def squares_a_list(numerical_list):
+
+ print(a_new_variable)
+
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```{python}
+squares_a_list([12, 5, 7, 99999])
+```
+
+
+:::{.notes}
+Global variables differ from local variables as they are not only recognized outside of any function but also recognized inside functions.
+
+Let's take a look at what happens when we add `a_new_variable`, which is a global variable,e and refer to it in the `squares_a_list` function.
+
+The function recognizes the global variable!
+
+It's important to note that, although functions recognize global variables, it's not good practice to have functions reference objects outside of it.
+
+We will learn more about this later in the module.
+:::
+
+---
+
+
+
+{fig-align="center" width="100%" fig-alt="404 image"}
+
+[Attribution - Starbucks](https://unsplash.com/photos/42ui88Qrxhw)
+
+[Attribution - 49th and Parallel](https://unsplash.com/photos/42ui88Qrxhw)
+
+
+:::{.notes}
+I'm going to make an analogy comparing coffee stores to variables.
+
+**Starbucks Coffee** is a ***globally*** recognized brand across the world and is available in 70 different countries.
+
+I can purchase a coffee from Starbucks in Vancouver (my local city), and if I were to travel across the world to Sydney, Australia, I would still be able to purchase a coffee from Starbucks there.
+
+Starbucks Coffee is similar to a global variable as it is accessible and recognized in both its local (Vancouver) and global environments.
+
+**49th Parallel** is a ***local*** Vancouver coffee store.
+
+Many people from Vancouver recognize it; however, purchasing a coffee from 49th Parallel outside of Vancouver would be impossible as it is not accessible past the City of Vancouver.
+
+Just like Starbucks Coffee, global variables are recognized and accessible in both their global and local environments, whereas local variables like the coffee store 49th Parallel are only recognized and accessible in the local environment it was created in.
+:::
+
+---
+
+## When things get tricky
+
+```{python}
+a_new_variable = "Peek-a-boo"
+```
+
+
+
+```{python}
+def squares_a_list(numerical_list):
+ a_new_variable = "Ta-Da!"
+ print(a_new_variable)
+
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```{python}
+squares_a_list([1, 2])
+```
+
+
+:::{.notes}
+Things can get unclear when we have variables that are named the same way but come from two different environments.
+
+What happens when 2 different objects share the same name, where one was defined inside the function and the other in the global environment?
+
+For instance, let's say we defined a variable `a_new_variable` in our global environment, and we've made a variable in a local environment with the same name `a_new_variable` but with different values within our `squares_a_list` function.
+
+We can see that the locally created `a_new_variable` variable was printed instead of the global object with the same name.
+:::
+
+---
+
+```{python}
+def squares_a_list(numerical_list):
+ a_new_variable = "Ta-Da!"
+ print(a_new_variable)
+
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```python
+squares_a_list([1, 2])
+a_new_variable
+```
+
+```out
+Ta-Da!
+'Peek-a-boo'
+```
+
+
+:::{.notes}
+What about if we output `a_new_variable` right after.
+
+Our function prints the locally defined `a_new_variable`, and the global environment prints the globally defined `a_new_variable`.
+:::
+
+---
+
+```{python}
+def squares_a_list(numerical_list, a_new_variable):
+ print(a_new_variable)
+
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```{python}
+a_new_variable = "Peek-a-boo"
+squares_a_list([1,2], "BAM!")
+```
+
+
+:::{.notes}
+What if `a_new_variable` was an argument?
+
+Given a global variable `a_new_variable = "Peek-a-boo"`, what value will the function print if we assign a value of `"BAM!"` to the input argument `a_new_variable`?
+
+Here we can see that the function uses the input argument value instead of the global variable value.
+:::
+
+---
+
+## Modifying global variables
+
+```{python}
+global_list = [50, 51, 52]
+```
+
+
+
+```{python}
+def squares_a_list(numerical_list):
+ global_list.append(99)
+ print("print global_list:", global_list)
+
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```{python error=TRUE}
+squares_a_list([1, 2])
+global_list
+```
+
+
+:::{.notes}
+So global variables are accessible inside functions - but what about modifying them?
+
+Let's take a list that we define in our global environment called `global_list` and add `99` to the list in the local environment.
+
+The list that we defined globally was able to be modified inside the function and have the changes reflected back in the global environment!
+
+What is going on?
+
+Modifying objects like this within a function without returning them is called a function **side effect**.
+:::
+
+---
+
+## Function Side Effects
+
+```{python}
+cereal = pd.read_csv('data/cereal.csv')
+cereal.head()
+```
+
+
+
+- `.drop()`
+- `.assign()`
+- `.sort_values()`
+- `.rename()`
+
+
+:::{.notes}
+For this next concept, we are going to bring back our trusty cereal dataframe.
+
+Since the beginning of this course, we have been using verbs such as;
+
+- `.drop()`
+- `.assign()`
+- `.sort_values()`
+- `.rename()`
+
+Where we modify a dataframe and save the modification as a new dataframe object.
+:::
+
+---
+
+```{python}
+cereal_dropped = cereal.drop(columns = ['sugars','potass','vitamins', 'shelf', 'weight', 'cups'])
+cereal_dropped.head(2)
+```
+
+
+
+```{python}
+cereal.head(2)
+```
+
+
+
+```{python}
+cereal.drop(columns = ['sugars','potass','vitamins', 'shelf', 'weight', 'cups'], inplace=True)
+```
+
+
+
+```{python}
+cereal.head(2)
+```
+
+
+:::{.notes}
+For example, when we have been dropping columns from a dataframe, we have been saving the changes with the assignment operator to a new object.
+
+In this example, we drop columns from `sugars` to `cups` and assign this modified dataframe with the dropped columns to the object named `cereal_dropped`.
+
+If we look at the original `cereal` dataframe, we can see it was unaffected by this transformation.
+
+Many of the verbs that we use for our transformations, such as the ones we mentioned on the previous slide, have an argument called `inplace`.
+
+The `inplace` argument accepts a Boolean value where the dataframe object is modified directly without the need to save the changes to an object with the assignment operation. That means we can skip the part of making a new object with the `=` sign.
+
+Let's try and drop the same columns as before but now using `inplace=True`.
+
+This time, nothing is returned when we execute this code; however, if we look at the `cereal` dataframe now, we can see that it's been altered, and the columns have been dropped.
+
+This transformation of the dataframe object is a **side effect** of the function.
+
+A side effect is when a function produces changes to global variables outside the environment it was created, this means a function has an observable effect besides the returning value.
+
+It's important to include that although `inplace` exists, there is a reason we haven't taught it, and it's because we don't recommend using it. Overriding the object by saving it with the same object name is the preferred coding technique.
+:::
+
+---
+
+```{python}
+# | eval: false
+cereal.to_csv('data/cereal.csv')
+```
+
+
+
+```{python}
+regular_list = [7, 8, 2020]
+regular_list
+```
+
+
+
+```{python}
+regular_list.append(3)
+```
+
+
+
+```{python}
+regular_list
+```
+
+
+:::{.notes}
+Although this appears to be new vocabulary, side effects have been present since the beginning of this course, starting with `pd.to_csv()`.
+
+`pd.to_csv()` is a function that we saw in module 1, that didn't return anything after we executed it but still produced a **side effect** of a newly saved csv file on our computer.
+
+Another example that we've seen when working with lists is the verb `.append()`.
+
+When we execute the code `.append(3)`, on our object `regular_list`, nothing is returned from the function, and we have not used to assignment operator to save any transformation to the list, however, when we inspect `regular_list`, we can see that it has been modified and included the new element `3`.
+
+This would be another example of a function with a side effect.
+
+The list was created in the global environment, but modified in `.append()`'s local environment.
+
+Side effects seem like fun, but they can be extremely problematic when trying to debug (fix) your code.
+
+When writing functions, it's usually a good idea to avoid side effects.
+
+If objects need to be modified, best practice is to modify them in the environment they originated in.
+:::
+
+---
+
+## Side Effect Documentation
+
+- If your functions have side-effects, they should be documented.
+
+
+:::{.notes}
+Although side effects are not recommended, there are cases where either we must have side-effects in our functions, or there is no way to avoid it. In these cases, it is extremely important that we document it.
+
+This leads to the next question of *How*? Good news - the answer is coming later on in this module!
+:::
+
+---
+
+## The deal with *print()*
+
+```{python}
+print('A regular string')
+```
+
+
+
+```{python}
+a_number_variable = 54.346
+
+print(a_number_variable)
+```
+
+
+:::{.notes}
+What is `print()`?
+
+We have not talked about this function in large detail but we do know `print()` will print whatever variable or item you call in it. It can be an especially handy one when debugging.
+
+We can use it to print some code directly or from a variable like we see here.
+
+It's important that we address using the print statement vs using return in a function as they are quite different.
+
+Let's see why.
+:::
+
+---
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+
+```{python}
+def squares_a_list_print(numerical_list):
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ print(new_squared_list)
+```
+
+
+:::{.notes}
+Here er have our `squares_a_list` function.
+Let's create a new function called `squares_a_list_print` where instead of returning the new variable `new_squared_list`, we print it instead.
+
+The only difference here is that in `squares_a_list` we return `new_squared_list` and in `squares_a_list_print` we are printing `new_squared_list`.
+:::
+
+---
+
+```{python}
+numbers = [2, 3, 5]
+```
+
+
+
+```{python}
+squares_a_list(numbers)
+```
+
+
+
+```{python}
+squares_a_list_print(numbers)
+```
+
+
+
+:::{.notes}
+Let's see what happens when we call these functions now.
+
+If we call them both without assigning them to an object, it looks like these functions do identical things.
+
+Both output the new list.
+:::
+
+---
+
+```{python}
+return_func_var = squares_a_list(numbers)
+```
+
+
+
+```{python}
+print(return_func_var)
+```
+
+
+
+```{python}
+print_func_var = squares_a_list_print(numbers)
+```
+
+
+
+```{python}
+print(print_func_var)
+```
+
+
+:::{.notes}
+This time let's instead save them to objects.
+
+When we call and save `squares_a_list(numbers)`, you'll see that nothing is printed or outputted.
+
+But if we print what our variable `return_func_var` contains, you'll see the list of square numbers.
+
+In contrast,when we do the same thing with `squares_a_list_print(numbers)`, the new list is outputted while we are assigning it to a variable since the print function is called within our `squares_a_list_print(numbers)` function.
+
+If we see print what's in our variables `print_func_var`, you'll see that there is nothing stored in it.
+
+That's because the `print()` function, when used in a function, is a **side effect** and our `squares_a_list_print()` function is not returning anything to store, it's only displaying it.
+
+In order for us to save the output of our functions to a variable, we must use `return` in our function, otherwise we are only producing a side effect instead of returning an actual value
+:::
+
+
+# Let’s apply what we learned!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_05.qmd b/modules/module6/slides/module6_05.qmd
new file mode 100644
index 00000000..52d9e555
--- /dev/null
+++ b/modules/module6/slides/module6_05.qmd
@@ -0,0 +1,258 @@
+---
+format: revealjs
+title: Function Arguments
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+
+ return new_squared_list
+```
+
+
+
+```{python}
+def cubes_a_list(numerical_list):
+ new_cubed_list = list()
+
+ for number in numerical_list:
+ new_cubed_list.append(number ** 3)
+
+ return new_cubed_list
+```
+
+
+:::{.notes}
+Let's talk more about function arguments.
+
+Arguments play a paramount role when it comes to adhering to the DRY principle as well as adding flexibility to your code.
+
+Let's bring back the function we made named `squares_a_list()`.
+
+The reason we made this function in the first place was to DRY out our code and avoid repeating the same `for` loop for any additional list we wished to operate on.
+
+What happens now if we no longer wanted to square a number but calculate a specified exponential of each element, perhaps n3, or n4?
+
+Would we need a new function?
+
+We could make a similar new function for cubing the numbers.
+
+But this feels repetitive.
+:::
+
+---
+
+```{python}
+def exponent_a_list(numerical_list, exponent):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+
+
+```{python}
+numbers = [2, 3, 5]
+exponent_a_list(numbers, 3)
+```
+
+
+
+```{python}
+exponent_a_list(numbers, 5)
+```
+
+
+:::{.notes}
+A better solution that adheres to the DRY principle is to tweak our original function but add an additional argument.
+
+Take a look at `exponent_a_list()` which now takes 2 arguments; the original `numerical_list`, and now a new argument named `exponent`.
+
+This gives us a choice of the exponent. We could use the same function now for any exponent we want instead of making a new function for each.
+
+This makes sense to do if we foresee needing this versatility, else the additional argument isn't necessary.
+:::
+
+---
+
+## Default Values for Arguments
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+
+
+```{python}
+numbers = [2, 3, 5]
+exponent_a_list(numbers)
+```
+
+
+:::{.notes}
+Python allows for default values in the event that an argument is not provided to the function.
+
+For example, in a new version of `exponent_a_list()`, the function uses a default value of `2` for `exponent`.
+
+Since we do not specify the exponent argument when we call our function, it defaults to an exponent of `2`.
+
+These arguments with default values are also called optional arguments (because you don't have to specify them), or "keyword arguments".
+:::
+
+---
+
+```{python}
+exponent_a_list(numbers, exponent=5)
+```
+
+
+
+```{python}
+exponent_a_list(numbers, 5)
+```
+
+
+:::{.notes}
+We also have the option of assigning this `exponent` argument something other than 2.
+
+We can specify a value for `exponent` using `exponent=5` if we need to.
+:::
+
+---
+
+```python
+def exponent_a_list(exponent=2, numerical_list):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+```out
+Error: parameter without a default follows parameter with a default (, line 1)
+```
+
+
+:::{.notes}
+Functions can have any number of arguments and any number of optional arguments, but we must be careful with the order of the arguments.
+
+When we define our arguments in a function, all arguments with default values (aka optional arguments) need to be placed ***after*** required arguments.
+
+If any required arguments follow any arguments with default values, an error will occur.
+
+Let's take our original function `exponent_a_list()` and re-order it so the optional `exponent` argument is defined first.
+
+We will see Python throw an error.
+:::
+
+---
+
+## Argument Ordering
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+
+
+```{python}
+exponent_a_list([2, 3, 5], 5)
+```
+
+
+:::{.notes}
+Up to this point, we have been calling functions with multiple arguments in a single way.
+
+When we call our function, we have been ordering the arguments in the order the function defined them in.
+
+So, in `exponent_a_list()`, the argument `numerical_list` is defined first, followed by the argument `exponent`.
+
+Naturally, we have been calling our function with the arguments in this order as well.
+:::
+
+---
+
+```{python}
+exponent_a_list([2, 3, 5], exponent= 5)
+```
+
+
+
+```{python}
+exponent_a_list(numerical_list=[2, 3, 5], exponent=5)
+```
+
+
+
+```{python}
+exponent_a_list(exponent=5, numerical_list=[2, 3, 5])
+```
+
+
+:::{.notes}
+We showed earlier that we could also call the function by specifying `exponent=5`.
+
+Another way of calling this would be to also specify any of the argument names that do not have default values, in this case, `numerical_list`.
+
+What happens if we switch up the order of the arguments and put `exponent=5` followed by `numerical_list=numbers`?
+
+It still works!
+:::
+
+---
+
+```{python}
+# | eval: false
+exponent_a_list(5, [2, 3, 5])
+```
+
+```out
+TypeError: 'int' object is not iterable
+
+Detailed traceback:
+ File "", line 1, in
+ File "", line 4, in exponent_a_list
+```
+
+
+:::{.notes}
+What about if we switch up the ordering of the arguments without specifying any of the argument names.
+
+Our function doesn’t recognize the input arguments, and an error occurs because the two arguments are being swapped - it thinks 5 is the list, and [2, 3, 5] is the exponent.
+
+It's important to take care when ordering and calling a function.
+
+The rule of thumb to remember is if you are going to call a function where the arguments are in a different order from how they were defined, you need to assign the argument name to the value when you call the function.
+:::
+
+
+# Let’s apply what we learned!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_09.qmd b/modules/module6/slides/module6_09.qmd
new file mode 100644
index 00000000..dfcea9eb
--- /dev/null
+++ b/modules/module6/slides/module6_09.qmd
@@ -0,0 +1,333 @@
+---
+format: revealjs
+title: Function Docstrings
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+
+ return new_squared_list
+
+```
+
+
+:::{.notes}
+Functions can get very complicated, so it is not always obvious what they do just from looking at the name, arguments, or code.
+
+Therefore, people like to explain what the function does.
+
+The standard format for doing this is called a **docstring**.
+
+A **docstring** is a literal string that comes directly after the function `def` and documents the function's purpose and usage.
+
+Writing a docstring documents what your code does so that collaborators (and you in 6 months' time!) are not struggling to decipher and reuse your code.
+
+In the last section we had our function `squares_a_list()`.
+
+Although our function name is quite descriptive, it could mean various things.
+
+How do we know what data type it takes in and returns?
+
+Having documentation for it can be useful in answering these questions.
+:::
+
+---
+
+{width="48%" fig-alt="404 image" fig-align="center"}
+
+
+:::{.notes}
+Here is the code for a function from the `pandas` package called `truncate()`.
+
+You can view the complete code here.
+
+I think we can all agree that it would take a bit of time to figure out what the function is doing, the expected input variable types, and what the function is returning.
+
+Luckily `pandas` provides detailed documentation to explain the function's code.
+:::
+
+---
+
+{width="34%" fig-alt="404 image" fig-align="center"}
+
+
+:::{.notes}
+Ah. This documentation gives us a much clearer idea of what the function is doing and how to use it.
+
+We can see what it requires as input arguments and what it returns.
+
+It also explains the expectations of the function.
+
+Reading this instead of the code saved us some time and definitely potential confusion.
+
+There are several styles of docstrings; this one and the one we'll be using is called the **NumPy** style.
+:::
+
+---
+
+```{python}
+string1 = """This is a string"""
+type(string1)
+```
+
+The NumPy format includes 4 main sections:
+
+- **A brief description of the function**
+- Explaining the input **Parameters**
+- What the function **Returns**
+- **Examples**
+
+
+:::{.notes}
+All docstrings, not just the Numpy formatted ones, are contained within 3 sets of quotations`"""`. We discussed in module 4 that this was one of the ways to implement string values.
+
+Adding this additional string to our function has no effect on our code, and the sole purpose of the docstring is for human consumption.
+
+The NumPy format includes 4 main sections:
+
+- **A brief description of the function**
+- Explaining the input **Parameters**
+- What the function **Returns**
+- **Examples**
+:::
+
+---
+
+## NumPy Format
+
+```{python}
+def squares_a_list(numerical_list):
+ """
+ Squared every element in a list.
+
+ Parameters
+ ----------
+ numerical_list : list
+ The list from which to calculate squared values
+
+ Returns
+ -------
+ list
+ A new list containing the squared value of each of the elements from the input list
+
+ Examples
+ --------
+ >>> squares_a_list([1, 2, 3, 4])
+ [1, 4, 9, 16]
+ """
+ new_squared_list = list()
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+ return new_squared_list
+```
+
+
+:::{.notes}
+Writing documentation for `squares_a_list()` using the **NumPy style** takes the following format.
+
+We can identify the brief description of the function at the top, the parameters that it takes in, and what object type they should be, as well as what to expect as an output.
+
+Here we can even see examples of how to run it and what is returned.
+:::
+
+---
+
+```python
+def function_name(param1, param2):
+ """The first line is a short description of the function.
+
+ A paragraph describing in a bit more detail what the function
+ does and what algorithms it uses and common use cases.
+
+ Parameters
+ ----------
+ param1 : datatype
+ A description of param1.
+ param2 : datatype
+ A longer description because maybe this requires
+ more explanation, and we can use several lines.
+
+ Returns
+ -------
+ datatype
+ A description of the output, data types, and behaviors.
+ Describe special cases and anything the user needs to know
+ to use the function.
+
+ Examples
+ --------
+ >>> function_name(3, 8, -5)
+ 2.0
+ """
+```
+
+
+:::{.notes}
+Functions using the NumPy docstring style follow this general form (reproduced from the SciPy/NumPy docs).
+:::
+
+---
+
+## Default Arguments
+
+```{python docstring}
+def exponent_a_list(numerical_list, exponent=2):
+ """
+ Creates a new list containing specified exponential values of the input list.
+
+ Parameters
+ ----------
+ numerical_list : list
+ The list from which to calculate exponential values from
+ exponent: int or float, optional
+ The exponent value (the default is 2, which implies the square).
+
+ Returns
+ -------
+ new_exponent_list : list
+ A new list containing the exponential value specified of each
+ of the elements from the input list
+
+ Examples
+ --------
+ >>> exponent_a_list([1, 2, 3, 4])
+ [1, 4, 9, 16]
+ """
+
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+
+:::{.notes}
+If our function contains optional arguments, we need to communicate this in our docstring.
+
+Using `exponent_a_list()`, a function from the previous section as an example, we include an *optional* note in the parameter definition and an explanation of the default value in the **parameter** description.
+:::
+
+---
+
+## Side Effects
+
+```{python}
+def function_name(param1, param2):
+ """The first line is a short description of the function.
+
+ If your function includes side effects, explain it clearly here.
+
+
+ Parameters
+ ----------
+ param1 : datatype
+ A description of param1.
+
+ .
+ .
+ .
+ Etc.
+ """
+```
+
+
+:::{.notes}
+Ah, remember how we talked about side effects back at the beginning of this module?
+
+Although we recommend avoiding side effects in your functions, there may be occasions where they're unavoidable or required.
+
+In these cases, we must make it clear in the documentation so that the user of the function knows that their objects are going to be modified.
+(As an analogy: If someone wants you to babysit their cat, you would probably tell them first if you were going to paint it red while you had it!)
+
+So how we include side effects in our docstrings?
+
+It's best to include your function side effects in the first sentence of the docstring.
+:::
+
+---
+
+## How to read a docstring
+
+```python
+?function_name
+```
+
+
+
+For example, if we want the docstring for the function `len()`:
+
+```python
+?len
+```
+
+```out
+Signature: len(obj, /)
+Docstring: Return the number of items in a container.
+Type: builtin_function_or_method
+```
+
+
+:::{.notes}
+Ok great! Now that we've written and explained our functions with a standardized format, we can read it in our file easily, but what if our function is located in a different file?
+
+How can we learn what it does when reading our code?
+
+We learned in the first assignment that we can read more about built-in functions using the question mark before the function name.
+
+This returns the docstring of the function.
+:::
+
+---
+
+```python
+?squares_a_list
+```
+
+```out
+Signature: squares_a_list(numerical_list)
+Docstring:
+Squared every element in a list.
+
+Parameters
+----------
+numerical_list : list
+ The list from which to calculate squared values
+
+Returns
+-------
+list
+ A new list containing the squared value of each of the elements from the input list
+
+Examples
+--------
+>>> squares_a_list([1, 2, 3, 4])
+[1, 4, 9, 16]
+File: ~/
+Type: function
+```
+
+
+:::{.notes}
+The same thing can be done to get the documentation of functions that we have defined.
+
+Getting the documentation for our function `squares_a_list()` is as easy as `?squares_a_list`.
+
+This returns the docstring that we created.
+:::
+
+
+# Let’s apply what we learned!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_13.qmd b/modules/module6/slides/module6_13.qmd
new file mode 100644
index 00000000..b37431d2
--- /dev/null
+++ b/modules/module6/slides/module6_13.qmd
@@ -0,0 +1,273 @@
+---
+format: revealjs
+title: Defensive Programming using Exceptions
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+
+***Defensive programming***: Code written in such a way that if errors do occur, they are handled in a graceful, fast and informative manner.
+
+***Exceptions***: Used in ***Defensive programming*** to disrupts the normal flow of instructions. When Python encounters code that it cannot execute, it will throw an ***exception***.
+
+
+:::{.notes}
+We all know that mistakes are a regular part of life.
+
+In coding, every line of code is at risk for potential errors, so naturally, we want a way of defending our functions against potential issues.
+
+***Defensive programming*** is code written in such a way that, if errors do occur, they are handled in a graceful, fast and informative manner.
+
+If something goes wrong, we don't want the code to crash on its own terms - we want it to fail gracefully, in a way we pre-determined.
+
+To help soften the landing, we write code that throws our own ***Exceptions***.
+
+***Exceptions*** are used in ***Defensive programming*** to disrupt the normal flow of instructions. When Python encounters code that it cannot execute, it will throw an ***exception***.
+:::
+
+---
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+ new_exponent_list = list()
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+ return new_exponent_list
+```
+
+
+
+```{python}
+# | eval: false
+numerical_string = "123"
+exponent_a_list(numerical_string)
+```
+
+```out
+TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
+
+Detailed traceback:
+ File "", line 1, in
+ File "", line 4, in exponent_a_list
+```
+
+
+:::{.notes}
+Before we dive into exceptions, let's revisit our function `exponent_a_list()`.
+
+It works somewhat well, but what happens if we try to use it with an input string instead of a list.
+
+We get an error that explains a little bit of what's causing the issue but not directly.
+
+This error, called a TypeError here, is itself a Python exception. But the error message, which is a default Python message, is not super clear.
+
+This is where raising our own `Exception` steps in to help.
+:::
+
+---
+
+## Exceptions
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+
+ if type(numerical_list) is not list:
+ raise Exception("You are not using a list for the numerical_list input.")
+
+ new_exponent_list = list()
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+ return new_exponent_list
+```
+
+
+
+```{python}
+# | eval: false
+numerical_string = "123"
+exponent_a_list(numerical_string)
+```
+
+```out
+Exception: You are not using a list for the numerical_list input.
+
+Detailed traceback:
+ File "", line 1, in
+ File "", line 4, in exponent_a_list
+```
+
+(Note that in the interest of avoiding new syntax, we are using a simple way of checking if an object is of a certain data type. For a more robust approach see here.)
+
+
+:::{.notes}
+Exceptions disrupt the regular execution of our code. When we raise an `Exception`, we are forcing our own error with our own message.
+
+If we wanted to raise an exception to solve the problem on the last slide, we could do the following.
+:::
+
+---
+
+
+
+{fig-alt="404 image" width="100%" fig-align="center"}
+
+
+:::{.notes}
+Let's take a closer look.
+
+The first line of code is an `if` statement - what needs to occur to trigger this new code we've written.
+
+This code translates to *"If `numerical_list` is not of the type `list`..."*.
+
+The second line does the complaining.
+
+We tell it to `raise` an `Exception` (throw an error) with this message.
+
+Now we get an error message that is straightforward on why our code is failing.
+
+`Exception: You are not using a list for the numerical_list input.`
+
+I hope we can agree that this message is easier to decipher than the original.
+
+The new message made the cause of the error much clearer to the user, making our function more usable.
+:::
+
+---
+
+## Why raise Exceptions
+
+```python
+if type(numerical_list) is not list:
+ raise Exception("You are not using a list for the numerical_list input.")
+```
+
+
+:::{.notes}
+Here we check if `numerical_list` is of the type we expect it to be, in this case, a `list`.
+
+Checking the datatype is a helpful exception since the user can quickly correct for a simple mistake.
+:::
+
+---
+
+## Exception types
+
+```python
+if type(numerical_list) is not list:
+ raise Exception("You are not using a list for the numerical_list input.")
+```
+
+ Here is a full list containing other types of exceptions available.
+
+
+:::{.notes}
+Let's now learn more about the possible different types of Exceptions.
+
+The exception type called `Exception` is a generic, catch-all exception type.
+
+There are also many other exception types; for example, you may have encountered `ValueError` or a `TypeError` at some point.
+
+`Exception`, which is used in our previous examples, may not be the best option for the raises we made.
+:::
+
+---
+
+ ```python
+if type(numerical_list) is not list:
+ raise Exception("You are not using a list for the numerical_list input.")
+```
+
+
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+
+ if type(numerical_list) is not list:
+ raise TypeError("You are not using a list for the numerical_list input.")
+
+ new_exponent_list = list()
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+ return new_exponent_list
+```
+
+
+
+```python
+numerical_string = "123"
+exponent_a_list(numerical_string)
+```
+
+```out
+TypeError: You are not using a list for the numerical_list input.
+
+Detailed traceback:
+ File "", line 1, in
+ File "", line 4, in exponent_a_list
+```
+
+For the full list of exception types take a look at this resource.
+
+
+:::{.notes}
+Let's take a look now at the exception we wrote that checks if the input value for `numerical_list` was the correct type.
+
+Since this is a type error, a better-raised exception over `Exception` would be `TypeError`.
+
+Let's make our correction here and change `Exception` in our function to `TypeError`.
+:::
+
+---
+
+## Exception Documentation
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+ """
+ Creates a new list containing specified exponential values of the input list.
+
+ Parameters
+ ----------
+ numerical_list : list
+ The list from which to calculate exponential values from
+ exponent : int or float, optional
+ The exponent value (the default is 2, which implies the square).
+
+ Returns
+ -------
+ new_exponent_list : list
+ A new list containing the exponential value specified of each
+ of the elements from the input list
+
+ Raises
+ ------
+ TypeError
+ If the input argument numerical_list is not of type list
+
+ Examples
+ --------
+ >>> exponent_a_list([1, 2, 3, 4])
+ [1, 4, 9, 16]
+ """
+```
+
+
+:::{.notes}
+Now that we can write exceptions, it's important to document them.
+
+It's a good idea to include details of any included exceptions in our function's docstring.
+
+Under the NumPy docstring format, we explain our raised exception after the "***Returns***" section.
+
+We first specify the exception type and then an explanation of what causes the exception to be raised.
+
+For example, we've added a "Raises" section in our `exponent_a_list` docstring here.
+:::
+
+
+# Let’s apply what we learned!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_17.qmd b/modules/module6/slides/module6_17.qmd
new file mode 100644
index 00000000..7c25ccbf
--- /dev/null
+++ b/modules/module6/slides/module6_17.qmd
@@ -0,0 +1,500 @@
+---
+format: revealjs
+title: Unit tests, corner cases
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+
+***How can we be so sure that the code we wrote is doing what we want it to?***
+
+***Does our code work 100% of the time?***
+
+{fig-align="center" width="25%" fig-alt="404 image"}
+
+These questions can be answered by using something called **units tests**.
+
+
+:::{.notes}
+In the last section, we learned about raising exceptions, which, in a lot of cases, helps the function user identify if they are using it correctly.
+
+But there are still some questions remaining:
+
+***How can we be so sure that the code we wrote is doing what we want it to?***
+
+***Does our code work 100% of the time?***
+
+These questions can be answered by using something called **units tests**.
+
+We'll be implementing unit tests in Python using `assert` statements." `assert` statements are just one way of implementing unit tests.
+
+Let's first discuss the syntax of an `assert` statement and then how they can be applied to the bigger concept, which is unit tests.
+:::
+
+---
+
+## Assert Statements
+
+```python
+assert 1 == 2 , "1 is not equal to 2."
+```
+
+```out
+AssertionError: 1 is not equal to 2.
+
+Detailed traceback:
+ File "", line 1, in
+```
+
+{fig-alt="404 image" width="75%" fig-align="center"}
+
+
+:::{.notes}
+`assert` statements can be used as sanity checks for our program.
+
+We implement them as a "debugging" tactic to make sure our code runs as we expect it to.
+
+When Python reaches an `assert` statement, it evaluates the condition to a Boolean value.
+
+If the statement is `True`, Python will continue to run. However, if the Boolean is `False`, the code stops running, and an error message is printed.
+
+Let's take a look at one.
+
+Here we have the keyword `assert` that checks if `1==2`. Since this is `False`, an error is thrown, and the message beside the condition `"1 is not equal to 2."` is outputted.
+:::
+
+---
+
+```{python}
+assert 1 == 1 , "1 is not equal to 1."
+print('Will this line execute?')
+```
+
+
+
+```{python}
+# | eval: false
+assert 1 == 2 , "1 is not equal to 2."
+print('Will this line execute?')
+```
+
+```out
+AssertionError: 1 is not equal to 2.
+
+Detailed traceback:
+ File "", line 1, in
+```
+
+
+:::{.notes}
+Let's take a look at an example where the Boolean is `True`.
+
+Here, since the `assert` statement results in a `True` values, Python continues to run, and the next line of code is executed.
+
+When an assert is thrown due to a Boolean evaluating to `False`, the next line of code does not get an opportunity to be executed.
+:::
+
+---
+
+```{python}
+# | eval: false
+assert 1 == 2
+```
+
+```out
+AssertionError:
+
+Detailed traceback:
+ File "", line 1, in
+```
+
+
+:::{.notes}
+Not all `assert` statements need to have a message.
+
+We can re-write the statement from before without one.
+
+This time you'll notice that the error doesn't contain the particular message beside `AssertionError` like we had before.
+:::
+
+---
+
+## Why?
+
+{fig-alt="404 image" width="100%" fig-align="center"}
+
+
+:::{.notes}
+Where do assert statements come in handy?
+
+Up to this point, we have been creating functions, and only after we have written them, we've tested if they work.
+
+Some programmers use a different approach: writing tests *before* the actual function. This is called Test-Driven Development.
+
+This may seem a little counter-intuitive, but we're creating the expectations of our function before the actual function code.
+
+Often we have an idea of what our function should be able to do and what output is expected.
+
+If we write our tests before the function, it helps understand exactly what code we need to write and it avoids encountering large time-consuming bugs down the line.
+
+Once we have a serious of tests for the function, we can put them into `assert` statements as an easy way of checking that all the tests pass.
+:::
+
+---
+
+## What to test?
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+
+
+```{python}
+assert exponent_a_list([1, 2, 4, 7], 2) == [1, 4, 16, 49], "incorrect output for exponent = 2"
+```
+
+
+
+```{python}
+assert exponent_a_list([1, 2, 3], 3) == [1, 8, 27], "incorrect output for exponent = 3"
+```
+
+
+
+```{python}
+assert type(exponent_a_list([1,2,4], 2)) == list, "output type not a list"
+```
+
+
+:::{.notes}
+So, what kind of tests do we want?
+
+We want to keep these tests simple - things that we know are true or could be easily calculated by hand.
+
+For example, let's look at our `exponent_a_list()` function.
+
+Easy cases for this function would be lists containing numbers that we can easily square or cube.
+
+For example, we expect the square output of `[1, 2, 4, 7]` to be `[1, 4, 16, 49]`.
+
+The test for this would look like the one shown here.
+
+It is recommended to write multiple tests.
+
+Let's write another test for a differently sized list as well as different values for both input arguments `numerical_list` and `exponent`.
+
+Let's make another test for `exponent` = `3`. Again, we use numbers that we know the cube of.
+
+We can also test that the type of the returned object is correct.
+:::
+
+---
+
+## False Positives
+
+```{python}
+def bad_function(numerical_list, exponent=2):
+ new_exponent_list = [numerical_list[0] ** exponent] # seed list with first element
+ for number in numerical_list[1:]:
+ new_exponent_list.append(number ** exponent)
+ return new_exponent_list
+```
+
+
+
+```{python}
+assert bad_function([1, 2, 4, 7], 2) == [1, 4, 16, 49], "incorrect output for exponent = 2"
+assert bad_function([2, 1, 3], 3) == [8, 1, 27], "incorrect output for exponent = 3"
+```
+
+
+
+```{python}
+# | eval: false
+bad_function([], 2)
+```
+
+```out
+IndexError: list index out of range
+
+Detailed traceback:
+ File "", line 1, in
+ File "", line 2, in bad_function
+```
+
+
+:::{.notes}
+Just because all our tests pass, this does not mean our program is necessarily correct.
+
+It's common that our tests can pass, but our code contains errors.
+
+Let's take a look at the function `bad_function()`. It's very similar to `exponent_a_list` except that it separately computes the first entry before doing the rest in the loop.
+
+This function looks like it would work perfectly fine, but what happens if we get an input argument for `numerical_list` that cannot be sliced?
+
+Let's write some unit tests using `assert` statements and see what happens.
+
+Here, it looks like our tests pass at first.
+
+But what happens if we try our function with an empty list?
+
+We get an unexpected error! How do we avoid this?
+
+Write a lot of tests and don't be overconfident, even after writing a lot of tests!
+
+Checking an empty list in our `bad_function()` function is an example of checking a **corner case**.
+
+A corner case is an input that is reasonable but a bit unusual and may trip up our code.
+:::
+
+---
+
+## Testing Functions that Work with Data
+
+```{python}
+def column_stats(df, column):
+ stats_dict = {'max': df[column].max(),
+ 'min': df[column].min(),
+ 'mean': round(df[column].mean()),
+ 'range': df[column].max() - df[column].min()}
+ return stats_dict
+```
+
+
+:::{.notes}
+Often, we will be making functions that work on data.
+
+For example, perhaps we want to write a function called `column_stats` that returns some summary statistics in the form of a dictionary.
+
+The function here is something we might have envisioned.
+(Note that if we're using test-driven development, this function will just be an idea, not completed code.)
+
+In these situations, we need to invent some sort of data so that we can easily calculate the max, min, range, and mean and write unit tests to check that our function does the correct operations.
+
+The data can be made from scratch using functions such as `pd.DataFrame()` or `pd.DataFrame.from_dict()` which we learned about in module 4.
+
+You can also upload a very small slice of an existing dataframe.
+:::
+
+---
+
+```{python}
+data = {'name': ['Cherry', 'Oak', 'Willow', 'Fir', 'Oak'],
+ 'height': [15, 20, 10, 5, 10],
+ 'diameter': [2, 5, 3, 10, 5],
+ 'age': [0, 0, 0, 0, 0],
+ 'flowering': [True, False, True, False, False]}
+
+forest = pd.DataFrame.from_dict(data)
+forest
+```
+
+
+
+```{python}
+assert column_stats(forest, 'height') == {'max': 20, 'min': 5, 'mean': 12.0, 'range': 15}
+assert column_stats(forest, 'diameter') == {'max': 10, 'min': 2, 'mean': 5.0, 'range': 8}
+assert column_stats(forest, 'age') == {'max': 0, 'min': 0, 'mean': 0, 'range': 0}
+```
+
+
+:::{.notes}
+The values we chose in our columns should be simple enough to easily calculate the expected output of our function.
+
+Just like how we made unit tests using calculations we know to be true, we do the same using a simple dataset we call **helper data**.
+
+The dataframe must have a small dimension to keep the calculations simple.
+
+The tests we write for the function `column_stats()` are now easy to calculate since the values we are using are few and simple.
+
+We wrote tests that check different columns in our `forest` dataframe.
+:::
+
+---
+
+## Systematic Approach
+
+We use a **systematic approach** to design our function using a general set of steps to follow when writing programs.
+
+
+***1. Write the function stub: a function that does nothing but accepts all input parameters and returns the correct datatype.***
+
+```python
+def exponent_a_list(numerical_list, exponent=2):
+ return list()
+```
+
+
+:::{.notes}
+We use a **systematic approach** to design our function using a general set of steps to follow when writing programs.
+
+The approach we recommend includes 5 steps:
+
+
+***1. Write the function stub: a function that does nothing but accepts all input parameters and returns the correct datatype.***
+
+This means we are writing the skeleton of a function.
+
+We include the line that defines the function with the input arguments and the `return` statement returning the object with the desired data type.
+
+Using our `exponent_a_list()` function as an example, we include the function's first line and the return statement.
+:::
+
+---
+
+***2. Write tests to satisfy the design specifications.***
+
+
+```{python}
+# | eval: false
+def exponent_a_list(numerical_list, exponent=2):
+ return list()
+
+assert type(exponent_a_list([1,2,4], 2)) == list, "output type not a list"
+assert exponent_a_list([1, 2, 4, 7], 2) == [1, 4, 16, 49], "incorrect output for exponent = 2"
+assert exponent_a_list([1, 2, 3], 3) == [1, 8, 27], "incorrect output for exponent = 3"
+```
+
+```out
+AssertionError: incorrect output for exponent = 2
+
+Detailed traceback:
+ File "", line 1, in
+```
+
+
+:::{.notes}
+***2. Write tests to satisfy the design specifications.***
+
+This is where our `assert` statements come in.
+
+We write tests that we want our function to pass.
+
+In our `exponent_a_list()` example, we expect that our function will take in a list and an optional argument named `exponent` and then returns a list with the exponential value of each element of the input list.
+
+Here we can see our code fails since we have no function code yet!
+:::
+
+---
+
+***3. Outline the program with pseudo-code.***
+
+```{python}
+# | eval: false
+def exponent_a_list(numerical_list, exponent=2):
+
+ # create a new empty list
+ # loop through all the elements in numerical_list
+ # for each element calculate element ** exponent
+ # append it to the new list
+
+ return list()
+
+assert type(exponent_a_list([1,2,4], 2)) == list, "output type not a list"
+assert exponent_a_list([1, 2, 4, 7], 2) == [1, 4, 16, 49], "incorrect output for exponent = 2"
+assert exponent_a_list([1, 2, 3], 3) == [1, 8, 27], "incorrect output for exponent = 3"
+```
+
+```out
+AssertionError: incorrect output for exponent = 2
+
+Detailed traceback:
+ File "", line 1, in
+```
+
+
+:::{.notes}
+***3. Outline the program with pseudo-code.***
+
+Pseudo-code is an informal but high-level description of the code and operations that we wish to implement.
+
+In this step, we are essentially writing the steps that we anticipate needing to complete our function as comments within the function.
+
+So for our function pseudo-code includes:
+
+ # create a new empty list
+ # loop through all the elements in numerical_list
+ # for each element calculate element ** exponent
+ # append it to the new list
+:::
+
+---
+
+***4. Write code and test frequently.***
+
+```{python}
+def exponent_a_list(numerical_list, exponent=2):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+
+assert type(exponent_a_list([1,2,4], 2)) == list, "output type not a list"
+assert exponent_a_list([1, 2, 4, 7], 2) == [1, 4, 16, 49], "incorrect output for exponent = 2"
+assert exponent_a_list([1, 2, 3], 3) == [1, 8, 27], "incorrect output for exponent = 3"
+```
+
+
+:::{.notes}
+***4. Write code and test frequently.***
+
+Here is where we fill in our function.
+
+As you work on the code, more and more tests of the tests that you wrote will pass until finally, all your `assert` statements no longer produce any error messages.
+:::
+
+---
+
+***5. Write documentation.***
+
+```python
+def exponent_a_list(numerical_list, exponent=2):
+ """ Creates a new list containing specified exponential values of the input list.
+
+ Parameters
+ ----------
+ numerical_list : list
+ The list from which to calculate exponential values from
+ exponent : int or float, optional
+ The exponent value (the default is 2, which implies the square).
+
+ Returns
+ -------
+ new_exponent_list : list
+ A new list containing the exponential value specified of each of
+ the elements from the input list
+
+ Examples
+ --------
+ >>> exponent_a_list([1, 2, 3, 4])
+ [1, 4, 9, 16]
+ """
+ new_exponent_list = list()
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+ return new_exponent_list
+```
+
+
+:::{.notes}
+***5. Write documentation.***
+
+Finally, we finish writing our function with a docstring.
+:::
+
+
+# Let’s apply what we learned!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_22.qmd b/modules/module6/slides/module6_22.qmd
new file mode 100644
index 00000000..ceb37f17
--- /dev/null
+++ b/modules/module6/slides/module6_22.qmd
@@ -0,0 +1,333 @@
+---
+format: revealjs
+title: Good Function Design Choices
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+## How to write good functions
+
+**What makes a function useful?**
+
+**Is a function more useful when it does more operations?**
+
+**Do adding parameters make your functions more or less functional?**
+
+These are all questions we need to think about when writing functions.
+
+
+:::{.notes}
+This has been quite a full module!
+
+We've learned how to make functions, how to handle errors gracefully, how to test our functions, and write the necessary documentation to keep our code comprehensible.
+
+These skills will all contribute to writing effective code.
+
+One thing we have not discussed yet is the actual code within a function.
+
+**What makes a function useful?**
+
+**Is a function more useful when it performs more operations?**
+
+**Does adding parameters make your functions more or less useful?**
+
+These are all questions we need to think about when writing functions.
+
+We are going to list some habits to adopt when writing and designing your functions.
+:::
+
+---
+
+## 1. Avoid "hard coding."
+
+**Hard coding** is the process of embedding values directly into your code without saving them in objects.
+
+```{python}
+def squares_a_list(numerical_list):
+ new_squared_list = list()
+
+ for number in numerical_list:
+ new_squared_list.append(number ** 2)
+
+ return new_squared_list
+```
+
+
+
+```{python}
+def exponent_a_list(numerical_list, exponent):
+ new_exponent_list = list()
+
+ for number in numerical_list:
+ new_exponent_list.append(number ** exponent)
+
+ return new_exponent_list
+```
+
+
+:::{.notes}
+**Hard coding** is the process of embedding values directly into your code without saving them in variables
+
+When we hardcode values into our code, it decreases flexibility.
+
+Being inflexible can cause you to end up writing more functions and/or violating the DRY principle.
+
+This, in turn, can decrease the readability and makes code problematic to maintain. In short, hard coding is a breeding ground for bugs.
+
+Remember our function `squares_a_list()`?
+
+In this function, we "hard-coded" in `2` when we calculated `number ** 2`.
+
+There are a couple of approaches to improving the situation. One is to assign 2 to a variable in the function before doing this calculation. That way, if you need to reuse that number, later on, you can just refer to the variable; and if you need to change the 2 to a 3, you only need to change it in one place. Another benefit is that you're giving it a variable name, which acts as a little bit of documentation.
+
+The other approach is to turn the value into an argument like we did when we made `exponent_a_list()`.
+
+This new function now gives us more flexibility with our code.
+
+If we now encounter a situation where we need to calculate each element to a different exponent like 4 or 0, we can do so without writing new code and potentially making a new error in doing so.
+
+We reduce our long term workload.
+
+This version is more maintainable code, but it doesn't give the function caller any flexibility. What you decide depends on how you expect your function to be used.
+:::
+
+---
+
+## 2. Less is More
+
+```{python}
+def load_filter_and_average(file, grouping_column, ploting_column):
+ df = pd.read_csv("data/" + file)
+ source = df.groupby(grouping_column).mean(numeric_only=True).reset_index()
+ chart = alt.Chart(source, width = 500, height = 300).mark_bar().encode(
+ x=alt.X(grouping_column),
+ y=alt.Y(ploting_column)
+ )
+ return chart
+
+```
+
+
+
+```{python}
+# | output: false
+bad_idea = load_filter_and_average('cereal.csv', 'mfr', 'rating')
+bad_idea
+```
+
+
+
+```{python}
+# | include: false
+bad_idea.save('static/module6/chart_bad_idea.png')
+```
+
+{fig-alt="404 image" width="40%"}
+
+
+:::{.notes}
+Although it may seem useful when a function acts as a one-stop-shop that does everything you want in a single function, this also limits your ability to reuse code that lies within it.
+
+Ideally, functions should serve a single purpose.
+
+For example, let's say we have a function that reads in a csv, finds the mean of each group in a column, and plots a specified variable.
+
+Although this may seem nice, we may want to break this up into multiple smaller functions. For example, what if we don't want the plot? Perhaps the plot is just something we wanted a single time, and now we are committed to it for each time we use the function.
+
+Another problem with this function is that the means are only printed and not returned. Thus, we have no way of accessing the statistics to use further in our code (we would have to repeat ourselves and rewrite it).
+:::
+
+---
+
+```{python}
+def grouped_means(df, grouping_column):
+ grouped_mean = df.groupby(grouping_column).mean(numeric_only=True).reset_index()
+ return grouped_mean
+```
+
+
+
+```{python}
+# | include: false
+cereal = pd.read_csv('data/cereal.csv')
+```
+
+```{python}
+cereal_mfr = grouped_means(cereal, 'mfr')
+cereal_mfr
+```
+
+
+:::{.notes}
+In this case, you want to simplify the function.
+
+Having a function that only calculates the mean values of the groups in the specified column is much more usable.
+
+A preferred function would look something like this, where the input is a dataframe we have already read in, and the output is the dataframe of mean values for all the columns.
+:::
+
+---
+
+```{python}
+def plot_mean(df, grouping_column, ploting_column):
+ chart = alt.Chart(df, width = 500, height = 300).mark_bar().encode(
+ x=alt.X(grouping_column),
+ y=alt.Y(ploting_column)
+ )
+ return chart
+```
+
+
+
+```{python}
+# | output: false
+plot1 = plot_mean(cereal_mfr, 'mfr', 'rating')
+plot1
+```
+
+```{python}
+# | include: false
+plot1.save('static/module6/plot_better.png')
+```
+
+{fig-alt="404 image" width="50%"}
+
+
+:::{.notes}
+If we wanted, we could then make a second function that creates the desired plot part of the previous function.
+:::
+
+---
+
+## 3. Return a single object
+
+```{python}
+# | include: false
+pd.set_option('display.max_columns', 6)
+```
+
+```{python}
+def load_filter_and_average(file, grouping_column, ploting_column):
+ df = pd.read_csv("data/" + file)
+ source = df.groupby(grouping_column).mean(numeric_only=True).reset_index()
+ chart = alt.Chart(source, width = 500, height = 300).mark_bar().encode(
+ x=alt.X(grouping_column),
+ y=alt.Y(ploting_column)
+ )
+ return chart, source
+```
+
+
+
+```{python}
+another_bad_idea = load_filter_and_average('cereal.csv', 'mfr', 'rating')
+another_bad_idea
+```
+
+
+:::{.notes}
+For the most part, we have only lightly touched on the fact that functions can return multiple objects, and it's with good reason.
+
+Although functions are *capable* of returning multiple objects, that doesn't mean that it's the best option.
+
+For instance, what if we converted our function `load_filter_and_average()` so that it returns a dataframe ***and*** a plot.
+:::
+
+---
+
+```{python}
+# | output: false
+another_bad_idea[0]
+```
+
+```{python}
+# | include: false
+another_bad_idea[0].save('static/module6/badidea1.png')
+```
+
+{fig-alt="404 image" width="55%"}
+
+
+:::{.notes}
+Since our function returns a tuple, we can obtain the plot by selecting the first element of the output.
+
+This can be quite confusing. We would recommend separating the code into two functions and can have each one return a single object.
+
+It's best to think of programming functions in the same way as mathematical functions where most times, mathematical functions return a single value.
+:::
+
+---
+
+## 4. Keep global variables in their global environment
+
+```{python}
+def grouped_means(df, grouping_column):
+ grouped_mean = df.groupby(grouping_column).mean(numeric_only=True).reset_index()
+ return grouped_mean
+```
+
+
+
+```{python}
+cereal = pd.read_csv('data/cereal.csv')
+
+def bad_grouped_means(grouping_column):
+ grouped_mean = cereal.groupby(grouping_column).mean(numeric_only=True).reset_index()
+ return grouped_mean
+```
+
+
+:::{.notes}
+It's generally bad form to include objects in a function that were created outside of it.
+
+Take our `grouped_means()` function.
+
+What if instead of including `df` as an input argument, we just used `cereal` that we loaded earlier?
+
+The number one problem with doing this is now our function only works on the cereal data - it's not usable on other data.
+:::
+
+---
+
+```{python}
+bad_cereal_grouping = bad_grouped_means('mfr')
+bad_cereal_grouping.head(3)
+```
+
+
+
+```python
+cereal = "let's change it to a string"
+bad_cereal_grouping = bad_grouped_means('mfr')
+```
+
+```out
+AttributeError: 'str' object has no attribute 'groupby'
+
+Detailed traceback:
+ File "", line 1, in
+ File "", line 2, in bad_grouped_means
+```
+
+
+:::{.notes}
+Ok, let's say we still use it, then what happens?
+
+Although it does work, global variables have the opportunity to be altered in the global environment.
+
+When we change the global variable outside the function and try to use the function again, it will refer to the new global variable and potentially no longer work.
+
+Of course, like in any case, these habits are suggestions and not strict rules.
+
+There will be times where adhering to one of these may not be possible or will hinder your code instead of enhancing it.
+
+The rule of thumb is to ask yourself how helpful is your function if you or someone else wishes to reuse it.
+:::
+
+
+# Let’s apply what we learned!
\ No newline at end of file
diff --git a/modules/module6/slides/module6_26.qmd b/modules/module6/slides/module6_26.qmd
new file mode 100644
index 00000000..efbc7952
--- /dev/null
+++ b/modules/module6/slides/module6_26.qmd
@@ -0,0 +1,42 @@
+---
+format: revealjs
+title: What Did we Learn and What to Expect in Assignment 6
+title-slide-attributes:
+ data-notes: |
+---
+
+```{python}
+# | echo: false
+%run src/utils.py
+```
+
+## Summary
+
+Students are now expected to be able to:
+
+- Evaluate the readability, complexity and performance of a function.
+- Write docstrings for functions following the NumPy/SciPy format.
+- Write comments within a function to improve readability.
+- Write and design functions with default arguments.
+- Explain the importance of scoping and environments in Python as they relate to functions.
+- Formulate test cases to prove a function design specification.
+- Use `assert` statements to formulate a test case to prove a function design specification.
+- Use test-driven development principles to define a function that accepts parameters, returns values and passes all tests.
+- Handle errors gracefully via exception handling.
+
+
+:::{.notes}
+The assignment will concentrate on the learning objectives as well as building knowledge on existing concepts.
+:::
+
+---
+
+## Attribution
+
+The cereal dataset:
+
+ “[80 Cereals](https://www.kaggle.com/crawford/80-cereals/)” (c) by [Chris Crawford](https://www.linkedin.com/in/crawforc3/) is licensed
+under [Creative Commons Attribution-ShareAlike 3.0 Unported](http://creativecommons.org/licenses/by-sa/3.0/)
+
+
+# On to Assignment 6!
\ No newline at end of file