diff --git a/sum23/lecture_materials/13_Copying/lec-13-worksheet-solutions.pdf b/sum23/lecture_materials/13_Copying/lec-13-worksheet-solutions.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..2cd79bf69b38e9871d7e64fa6a069d979f78e283
Binary files /dev/null and b/sum23/lecture_materials/13_Copying/lec-13-worksheet-solutions.pdf differ
diff --git a/sum23/lecture_materials/13_Recursion/lec_13_recursion_notes.ipynb b/sum23/lecture_materials/13_Recursion/lec_13_recursion_notes.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..4367f61289192290d7bf12b748bc5f6ff004cc87
--- /dev/null
+++ b/sum23/lecture_materials/13_Recursion/lec_13_recursion_notes.ipynb
@@ -0,0 +1,865 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Recursion"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Review 1\n",
+    "\n",
+    "- Why does Python have the complexity of separate references and objects?\n",
+    "- Why not follow the original organization we saw for everything (i.e., boxes of data with labels)?\n",
+    "    - Reason 1: Performance\n",
+    "    - Reason 2: Centralized Updates"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Before\n",
+      "After\n",
+      "End\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Reason 1: Performance\n",
+    "# Try this example in PythonTutor\n",
+    "\n",
+    "print(\"Before\")\n",
+    "x = \"this string is millions of characters\" + \"!\" * (10 ** 6)\n",
+    "print(\"After\")\n",
+    "y = x # this is fast! Why? \n",
+    "print(\"End\")\n",
+    "      # Answer: Recall that assignment just creates a reference copy"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Reason 2: Centralized Updates\n",
+    "\n",
+    "# Try this example in PythonTutor\n",
+    "alice = {\"name\":\"Alice\", \"score\":10, \"age\":30}\n",
+    "bob = {\"name\":\"Bob\", \"score\":8, \"age\":25}\n",
+    "winner = alice\n",
+    "\n",
+    "alice[\"age\"] += 1\n",
+    "print(\"Winner age:\", winner[\"age\"]) \n",
+    "# what line 9 will output?\n",
+    "# Answer: 31"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Review 2\n",
+    "\n",
+    "Assignment creates reference copy immaterial of whether it is into a variable or into another data structure. Recall that references can be stored either in a variable or inside object instances for other data structures."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Try this example in PythonTutor\n",
+    "\n",
+    "alice = {\"name\":\"Alice\", \"score\":10, \"age\":30}\n",
+    "bob = {\"name\":\"Bob\", \"score\":8, \"age\":25}\n",
+    "team = [alice, bob]              \n",
+    "# TODO: discuss does this create new inner dictionaries?\n",
+    "# Answer: this line only create a new object for outer \n",
+    "# list, which directly stores references to existing\n",
+    "# dictionary object instances\n",
+    "\n",
+    "players = {\"A\": alice, \"B\": bob} \n",
+    "# TODO: discuss does this create new inner dictionaries?\n",
+    "# Answer: this line only create a new object for outer \n",
+    "# dictionary, which directly stores references to \n",
+    "# existing dictionary object instances"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Review 3"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Use 'in' to determine if the given thing is in my_list\n",
+    "my_list = [\"meet\", \"me\", \"after\", \"2:00pm\"]\n",
+    "print(\"me\" in my_list)       # TODO: predict the output\n",
+    "print(\"Meena\" in my_list)    # TODO: predict the output"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "False\n",
+      "False\n",
+      "True\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Let's try a nested list\n",
+    "my_list = [11, \"meet\", [\"me\", \"them\", \"us\"],  [84, 19, 22], \"school\", 2.54]\n",
+    "print(\"me\" in my_list)       # TODO: predict the output\n",
+    "print(84 in my_list)         # TODO: predict the output\n",
+    "print(11 in my_list)         # TODO: predict the output"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Warmup 1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[11, 'meet', ['me', 'them', 'us'], [84, 19, 22], 'school', 2.54]\n",
+      "True\n",
+      "True\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "def search_list_depth2(target, some_list):\n",
+    "    ''' returns True if thing in some_list, False otherwise'''\n",
+    "    for thing in some_list:\n",
+    "        #print(thing, type(thing))\n",
+    "        if type(thing) != list:\n",
+    "            if target == thing:\n",
+    "                return True\n",
+    "            else:\n",
+    "                continue # do we need this? (answer: no)\n",
+    "        else: # its a list\n",
+    "            if target in thing:\n",
+    "                return True\n",
+    "            else: \n",
+    "                continue # do we need this? (answer: no)\n",
+    "                \n",
+    "    return False  # after all possible searching....not found\n",
+    "\n",
+    "print(my_list)\n",
+    "print(search_list_depth2(\"school\", my_list))  # in list\n",
+    "print(search_list_depth2(22, my_list))        # in nested list\n",
+    "print(search_list_depth2(\"house\", my_list))   # not anywhere"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Warmup 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True\n",
+      "False\n",
+      "True\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "list_3_deep = [22, [33, 44, [55, 66], 77], 88]\n",
+    "\n",
+    "# let's try it our previous function\n",
+    "print(search_list_depth2(22, list_3_deep))  # in list\n",
+    "print(search_list_depth2(99, list_3_deep))  # not in list\n",
+    "\n",
+    "# write other tests to be sure that it works\n",
+    "print(search_list_depth2(33, list_3_deep))  # in nested list\n",
+    "print(search_list_depth2(55, list_3_deep))  # in nested nested list"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def search_list_depth3(target, some_list):\n",
+    "    ''' returns True if thing in some_list, False otherwise\n",
+    "        NOTE: Anna hasn't checked if this code works (it probably isn't perfect yet)\n",
+    "            The point is just to illustate that as the list gets deeper, we have to add more\n",
+    "            layers of nested if statements\n",
+    "    '''\n",
+    "    for thing in some_list:\n",
+    "        #print(thing, type(thing))\n",
+    "        if type(thing) != list:\n",
+    "            if target == thing:\n",
+    "                return True\n",
+    "            else:\n",
+    "                continue # do we need this? (answer: no)\n",
+    "        else: # its a list\n",
+    "            if target in thing:\n",
+    "                return True\n",
+    "            for thing2 in thing:\n",
+    "                if type(thing) != list:\n",
+    "                    if target == thing:\n",
+    "                        return True\n",
+    "                    else:\n",
+    "                        continue # do we need this? (answer: no)\n",
+    "                else: # its a list\n",
+    "                    if target in thing:\n",
+    "                        return True\n",
+    "                \n",
+    "    return False  # after all possible searching....not found\n",
+    "\n",
+    "print(my_list)\n",
+    "print(search_list_depth2(\"school\", my_list))  # in list\n",
+    "print(search_list_depth2(22, my_list))        # in nested list\n",
+    "print(search_list_depth2(\"house\", my_list))   # not anywhere"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# what about ANY depth list? \n",
+    "# that is the goal of today's lecture"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Learning Objectives\n",
+    "\n",
+    "After today's Lecture you will be able to: \n",
+    "\n",
+    "Define recursion and be able to identify\n",
+    "- base case\n",
+    "- recursive case\n",
+    "- infinite recursion\n",
+    "\n",
+    "Explain why the following can be recursively defined\n",
+    "- lists\n",
+    "- dictionaries\n",
+    "\n",
+    "Trace a recursive function\n",
+    "- involving numeric computation\n",
+    "- involving nested data structures"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<div>\n",
+    "<img src=\"attachment:Droste_effect.png\" width=\"450\"/>\n",
+    "</div>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Announcements\n",
+    "Quiz 6 is due today\n",
+    "\n",
+    "Project deadlines are unusual for P8, P9, P10 (use the extra time next week to study for the exam)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Recursion is defined as the process of defining something in terms of itself.\n",
+    "\n",
+    "**Good example:**\n",
+    "\n",
+    "*Hofstadter's Law*: “It always takes longer than you expect, even when you take into account *Hofstadter's Law*.” (From Gödel, Escher, Bach)\n",
+    "\n",
+    "**Unhelpful self-reference example:**\n",
+    "\n",
+    "*mountain*: “a landmass that projects conspicuously above its surroundings and is higher than a *hill*”\n",
+    "\n",
+    "*hill*: “a usually rounded natural elevation of land lower than a *mountain*”\n",
+    "(From Merriam-Webster dictionary)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "**Mathematical example:**\n",
+    "\n",
+    "A number x is a *positive even number* if:\n",
+    "- x is 2 or\n",
+    "- x equals another *positive even number* plus two\n",
+    "\n",
+    "<div>\n",
+    "<img src=\"attachment:Recursive%20structure.png\" width=\"450\"/>\n",
+    "</div>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "**Term**: branch\n",
+    "\n",
+    "**Definition**: wooden stick, with an end splitting into other *branches*, OR terminating with a leaf\n",
+    "\n",
+    "\n",
+    "<div>\n",
+    "<img src=\"attachment:Trees.png\" width=\"400\"/>\n",
+    "</div>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<div>\n",
+    "<img src=\"attachment:Base_case_recursive_case.png\" width=\"250\"/>\n",
+    "</div>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "**Term**: directory\n",
+    "\n",
+    "**Definition**: a collection of files and *directories*\n",
+    "    \n",
+    "<div>\n",
+    "<img src=\"attachment:Directory_structure-2.png\" width=\"450\"/>\n",
+    "</div>    "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Define recursion and be able to identify \n",
+    "- base case\n",
+    "- recursive case\n",
+    "- infinite recursion"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Factorial of a number"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Goal: work from examples to get to recursive code\n",
+    "\n",
+    "#### Step 1: Come up with examples\n",
+    "\n",
+    "```python\n",
+    "1! = 1\n",
+    "2! = 1*2 = 2\n",
+    "3! = 1*2*3 = 6\n",
+    "4! = 1*2*3*4 = 24\n",
+    "5! = 1*2*3*4*5 = 120\n",
+    "```\n",
+    "\n",
+    "#### Step 2: Identify self-reference\n",
+    "```python\n",
+    "1! =  # don't need a pattern at the start\n",
+    "2! = \n",
+    "3! = \n",
+    "4! = \n",
+    "5! = \n",
+    "```\n",
+    "\n",
+    "#### Step 3: Recursive definition\n",
+    "```python\n",
+    "1! is ???\n",
+    "N! is ??? for N > 1\n",
+    "```\n",
+    "\n",
+    "#### Step 4: Convert to Python code\n",
+    "- **Rule 1**: Base case should always be defined and be terminal\n",
+    "- **Rule 2**: Recursive case should make progress towards base case"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def fact(n):\n",
+    "    if n == 1:\n",
+    "        return 1\n",
+    "    p = fact(n-1)                                   \n",
+    "    return n * p"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### How does Python keep all the variables separate?\n",
+    "- Frames\n",
+    "\n",
+    "<div>\n",
+    "<img src=\"attachment:Frames.png\" width=\"450\"/>\n",
+    "</div> "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Try this in PythonTutor\n",
+    "fact(3)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<div>\n",
+    "<img src=\"attachment:Factorial.png\" width=\"500\"/>\n",
+    "</div> "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### RecursionError\n",
+    "\n",
+    "#### If there is no base case what happens in the above example? \n",
+    "- recursion never ends......infinite recursion\n",
+    "\n",
+    "#### infinite recursion can also happen if the recursive case does not move towards the base"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "ename": "RecursionError",
+     "evalue": "maximum recursion depth exceeded",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mRecursionError\u001b[0m                            Traceback (most recent call last)",
+      "Cell \u001b[0;32mIn[2], line 6\u001b[0m\n\u001b[1;32m      4\u001b[0m     p \u001b[38;5;241m=\u001b[39m bad_fact(n\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m)                                   \n\u001b[1;32m      5\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n\u001b[0;32m----> 6\u001b[0m \u001b[43mbad_fact\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\n",
+      "Cell \u001b[0;32mIn[2], line 4\u001b[0m, in \u001b[0;36mbad_fact\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mbad_fact\u001b[39m(n):\n\u001b[1;32m      2\u001b[0m     \u001b[38;5;66;03m#if n == 1:\u001b[39;00m\n\u001b[1;32m      3\u001b[0m     \u001b[38;5;66;03m#    return 1\u001b[39;00m\n\u001b[0;32m----> 4\u001b[0m     p \u001b[38;5;241m=\u001b[39m \u001b[43mbad_fact\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m                                   \n\u001b[1;32m      5\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n",
+      "Cell \u001b[0;32mIn[2], line 4\u001b[0m, in \u001b[0;36mbad_fact\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mbad_fact\u001b[39m(n):\n\u001b[1;32m      2\u001b[0m     \u001b[38;5;66;03m#if n == 1:\u001b[39;00m\n\u001b[1;32m      3\u001b[0m     \u001b[38;5;66;03m#    return 1\u001b[39;00m\n\u001b[0;32m----> 4\u001b[0m     p \u001b[38;5;241m=\u001b[39m \u001b[43mbad_fact\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m                                   \n\u001b[1;32m      5\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n",
+      "    \u001b[0;31m[... skipping similar frames: bad_fact at line 4 (2970 times)]\u001b[0m\n",
+      "Cell \u001b[0;32mIn[2], line 4\u001b[0m, in \u001b[0;36mbad_fact\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mbad_fact\u001b[39m(n):\n\u001b[1;32m      2\u001b[0m     \u001b[38;5;66;03m#if n == 1:\u001b[39;00m\n\u001b[1;32m      3\u001b[0m     \u001b[38;5;66;03m#    return 1\u001b[39;00m\n\u001b[0;32m----> 4\u001b[0m     p \u001b[38;5;241m=\u001b[39m \u001b[43mbad_fact\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m                                   \n\u001b[1;32m      5\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n",
+      "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded"
+     ]
+    }
+   ],
+   "source": [
+    "def bad_fact(n):\n",
+    "    #if n == 1:\n",
+    "    #    return 1\n",
+    "    p = bad_fact(n-1)                                   \n",
+    "    return n * p\n",
+    "bad_fact(3)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "ename": "RecursionError",
+     "evalue": "maximum recursion depth exceeded in comparison",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mRecursionError\u001b[0m                            Traceback (most recent call last)",
+      "Cell \u001b[0;32mIn[3], line 6\u001b[0m\n\u001b[1;32m      4\u001b[0m     p \u001b[38;5;241m=\u001b[39m bad_fact2(n)                                   \n\u001b[1;32m      5\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n\u001b[0;32m----> 6\u001b[0m \u001b[43mbad_fact2\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\n",
+      "Cell \u001b[0;32mIn[3], line 4\u001b[0m, in \u001b[0;36mbad_fact2\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m      3\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m----> 4\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43mbad_fact2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m)\u001b[49m                                   \n\u001b[1;32m      5\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n",
+      "Cell \u001b[0;32mIn[3], line 4\u001b[0m, in \u001b[0;36mbad_fact2\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m      3\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m----> 4\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43mbad_fact2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m)\u001b[49m                                   \n\u001b[1;32m      5\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n",
+      "    \u001b[0;31m[... skipping similar frames: bad_fact2 at line 4 (2969 times)]\u001b[0m\n",
+      "Cell \u001b[0;32mIn[3], line 4\u001b[0m, in \u001b[0;36mbad_fact2\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m      3\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m----> 4\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43mbad_fact2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m)\u001b[49m                                   \n\u001b[1;32m      5\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m p\n",
+      "Cell \u001b[0;32mIn[3], line 2\u001b[0m, in \u001b[0;36mbad_fact2\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mbad_fact2\u001b[39m(n):\n\u001b[0;32m----> 2\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m==\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m:\n\u001b[1;32m      3\u001b[0m         \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[1;32m      4\u001b[0m     p \u001b[38;5;241m=\u001b[39m bad_fact2(n)                                   \n",
+      "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded in comparison"
+     ]
+    }
+   ],
+   "source": [
+    "def bad_fact2(n):\n",
+    "    if n == 1:\n",
+    "        return 1\n",
+    "    p = bad_fact2(n)                                   \n",
+    "    return n * p\n",
+    "bad_fact2(3)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Self-check: Tracing example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Tracing a recursive function in the Python Tutor\n",
+    "# do this on your own\n",
+    "# Example 1\n",
+    "\n",
+    "def foo(n):\n",
+    "    # I always start recursive functions by printing the parameters\n",
+    "    print(\"Starting foo with n= \" , n)\n",
+    "    if n < 0:\n",
+    "        print(\"base case, returning 100\")\n",
+    "        return 100\n",
+    "    else:\n",
+    "        temp = foo(n-2)\n",
+    "        print(\"Ending foo returning \" ,  (n + temp))\n",
+    "        return n + temp\n",
+    "\n",
+    "foo(13)\n",
+    "\n",
+    "# What happens if we replace < with == ? \n",
+    "#    Recursion error if the original n is odd"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "in collatz with n =  13\n",
+      "in the odd case\n",
+      "in collatz with n =  40\n",
+      "In the even case\n",
+      "in collatz with n =  20\n",
+      "In the even case\n",
+      "in collatz with n =  10\n",
+      "In the even case\n",
+      "in collatz with n =  5\n",
+      "in the odd case\n",
+      "in collatz with n =  16\n",
+      "In the even case\n",
+      "in collatz with n =  8\n",
+      "In the even case\n",
+      "in collatz with n =  4\n",
+      "In the even case\n",
+      "in collatz with n =  2\n",
+      "In the even case\n",
+      "in collatz with n =  1\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "1"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Example 2\n",
+    "# The Collatz Conjecture problem \n",
+    "# https://en.wikipedia.org/wiki/Collatz_conjecture\n",
+    "# run this in Python Tutor on your ownn\n",
+    "\n",
+    "def collatz(n):\n",
+    "    # I always start recursive functions by printing the parameters\n",
+    "    print(\"in collatz with n = \" ,  n)\n",
+    "    if n == 1:\n",
+    "        return 1 # base case\n",
+    "    elif n % 2 == 0:\n",
+    "        print(\"In the even case\")\n",
+    "        return collatz(n//2)\n",
+    "    else:\n",
+    "        print(\"in the odd case\")\n",
+    "        return collatz (3*n+1)\n",
+    "\n",
+    "collatz(13)   # try other numbers\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Trace a recursive function involving nested data structures"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Write a recursive function to search *ANY* list of lists/tuples \n",
+    "# for a given word\n",
+    "def search_list_recursive(target, some_list):\n",
+    "    ''' returns True if target in some_list, False otherwise'''\n",
+    "    \n",
+    "    # base case 1: success!\n",
+    "    if target in some_list:\n",
+    "        return True\n",
+    "    \n",
+    "    # Strategy: iterate over all elements in some_list\n",
+    "    success = False\n",
+    "    for thing in some_list:\n",
+    "        # Because of base case 1, we know that thing is not target\n",
+    "        # TODO also want to include tuples\n",
+    "        if type(thing) == type([]):\n",
+    "            success = search_list_recursive(target, thing)\n",
+    "            if success:\n",
+    "                break\n",
+    "\n",
+    "    return success"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True\n",
+      "True\n",
+      "True\n",
+      "True\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "fav_stuff = [\"apples\", \"peaches\", \"oranges\", \n",
+    "             [\"A\", \"B\", \"C\", \"D\"],\n",
+    "             [[\"sedan\", \"SUV car\", \"minivan\"], \n",
+    "              [\"bicycle\", \"road bike\", \"scooter\"]]\n",
+    "            ]\n",
+    "\n",
+    "print(search_list_recursive(\"apples\", fav_stuff))   # outer list\n",
+    "print(search_list_recursive(\"D\", fav_stuff))        # list @ depth 1\n",
+    "print(search_list_recursive(\"road bike\", fav_stuff))# list @ depth 2\n",
+    "print(search_list_recursive(\"bicycle\", fav_stuff))  # list @ depth 2\n",
+    "print(search_list_recursive(\"pizza\", fav_stuff))    # doesn't exist"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[\n",
+      "A\n",
+      "\t[\n",
+      "\t1\n",
+      "\t2\n",
+      "\t3\n",
+      "\t]\n",
+      "B\n",
+      "\t[\n",
+      "\t4\n",
+      "\t\t[\n",
+      "\t\ti\n",
+      "\t\tii\n",
+      "\t\t]\n",
+      "\t]\n",
+      "]\n"
+     ]
+    }
+   ],
+   "source": [
+    "def pretty_print(items, indent = 0):\n",
+    "    \"\"\"\n",
+    "    prints the nested data structure with proper indentation\n",
+    "    \"\"\"\n",
+    "    # base case: not a list\n",
+    "    # edit: moving base case into a for loop\n",
+    "#     if type(items) != type([]):\n",
+    "#         print(indent * \"\\t\" + items)\n",
+    "#         return\n",
+    "    print(indent * \"\\t\" + \"[\")\n",
+    "    for i in items:\n",
+    "        \n",
+    "        # base case:\n",
+    "        if type(i) != type([]):\n",
+    "            print(indent * \"\\t\" + i)\n",
+    "        else:\n",
+    "            # recursive case\n",
+    "            pretty_print(i, indent + 1)\n",
+    "    print(indent * \"\\t\" + \"]\")  \n",
+    "\n",
+    "#data = [\"A\", \"B\", \"C\"]\n",
+    "data = [\"A\", [\"1\", \"2\", \"3\",], \"B\", [\"4\", [\"i\", \"ii\"]]]\n",
+    "pretty_print(data, 0)\n",
+    "             \n",
+    "# [ \"A\", \n",
+    "#   [ \"1\", \n",
+    "#     \"2\", \n",
+    "#     \"3\"], \n",
+    "#   \"B\", \n",
+    "# ], \n",
+    "             "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Explain why the following can be recursively defined\n",
+    "\n",
+    "- lists\n",
+    "- dictionaries\n",
+    "- JSON objects"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### dictionaries can have a recursive structure"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "person_info = { \"name\": \"Meena\", \n",
+    "               \"age\": 250, \n",
+    "               \"family\" : {\"spouse\": \"Rogers\", \n",
+    "                           \"child1\": {\"name\": \"Viyan\", \n",
+    "                                        \"age\": 2}, \n",
+    "                          }\n",
+    "              }"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# let's try to search through a deep dictionary. \n",
+    "def search_dict_recursive(target_key, some_dict):\n",
+    "    ''' returns the Value associated with target_key if tarket_key \n",
+    "    in any level of some_dict, None otherwise'''\n",
+    "    if target_key in some_dict:  # base case\n",
+    "        return some_dict[target_key]\n",
+    "    else:\n",
+    "        for key in some_dict:\n",
+    "            if type(some_dict[key]) == dict: # recursive case\n",
+    "                 return search_dict_recursive(target_key, \\\n",
+    "                                              some_dict[key])        \n",
+    "    return None\n",
+    "\n",
+    "print(search_dict_recursive(\"child1\", person_info))\n",
+    "print(search_dict_recursive(\"father\", person_info))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/sum23/lecture_materials/14_Comprehensions/lec_14_comprehensions_notes.ipynb b/sum23/lecture_materials/14_Comprehensions/lec_14_comprehensions_notes.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..1c61d62f8fbcf18e6377ec24ffc6d22dfc5a64ef
--- /dev/null
+++ b/sum23/lecture_materials/14_Comprehensions/lec_14_comprehensions_notes.ipynb
@@ -0,0 +1,1342 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Comprehensions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# import statements\n",
+    "import math\n",
+    "import csv"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Using `lambda`\n",
+    "- `lambda` functions are a way to abstract a function reference\n",
+    "- lambdas are simple functions with:\n",
+    "    - multiple possible parameters\n",
+    "    - single expression line as the function body\n",
+    "- lambdas are useful abstractions for:\n",
+    "    - mathematical functions\n",
+    "    - lookup operations\n",
+    "- lambdas are often associated with a collection of values within a list\n",
+    "- Syntax: \n",
+    "```python \n",
+    "lambda parameters: expression\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Let's sort the menu in different ways\n",
+    "- whenever you need to custom sort a dictionary, you must convert dict to list of tuples\n",
+    "- recall that you can use items method (applicable only to a dictionary)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'broccoli': 4.99,\n",
+       " 'orange': 1.19,\n",
+       " 'pie': 3.95,\n",
+       " 'donut': 1.25,\n",
+       " 'muffin': 2.25,\n",
+       " 'cookie': 0.79,\n",
+       " 'milk': 1.65,\n",
+       " 'bread': 5.99}"
+      ]
+     },
+     "execution_count": 2,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "menu = { \n",
+    "        'broccoli': 4.99,\n",
+    "        'orange': 1.19,\n",
+    "        'pie': 3.95, \n",
+    "        'donut': 1.25,    \n",
+    "        'muffin': 2.25,\n",
+    "        'cookie': 0.79,  \n",
+    "        'milk':1.65, \n",
+    "        'bread': 5.99}  \n",
+    "menu"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "dict_items([('broccoli', 4.99), ('orange', 1.19), ('pie', 3.95), ('donut', 1.25), ('muffin', 2.25), ('cookie', 0.79), ('milk', 1.65), ('bread', 5.99)])"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "menu.items()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort menu using item names (keys)\n",
+    "- let's first solve this using extract function\n",
+    "- recall that extract function deals with one of the inner items in the outer data structure\n",
+    "    - outer data structure is list\n",
+    "    - inner data structure is tuple"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def extract(menu_tuple):\n",
+    "    return menu_tuple[1]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('cookie', 0.79),\n",
+       " ('orange', 1.19),\n",
+       " ('donut', 1.25),\n",
+       " ('milk', 1.65),\n",
+       " ('muffin', 2.25),\n",
+       " ('pie', 3.95),\n",
+       " ('broccoli', 4.99),\n",
+       " ('bread', 5.99)]"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted(menu.items(), key = extract)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'cookie': 0.79,\n",
+       " 'orange': 1.19,\n",
+       " 'donut': 1.25,\n",
+       " 'milk': 1.65,\n",
+       " 'muffin': 2.25,\n",
+       " 'pie': 3.95,\n",
+       " 'broccoli': 4.99,\n",
+       " 'bread': 5.99}"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict(sorted(menu.items(), key = extract))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Now let's solve the same problem using lambdas\n",
+    "- if you are having trouble thinking through the lambda solution directly:\n",
+    "    - write an extract function\n",
+    "    - then abstract it to a lambda"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'cookie': 0.79,\n",
+       " 'orange': 1.19,\n",
+       " 'donut': 1.25,\n",
+       " 'milk': 1.65,\n",
+       " 'muffin': 2.25,\n",
+       " 'pie': 3.95,\n",
+       " 'broccoli': 4.99,\n",
+       " 'bread': 5.99}"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict(sorted(menu.items(), key = lambda menu_tuple : menu_tuple[1]))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort menu using prices (values)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# same as above\n",
+    "dict(sorted(menu.items(), key = lambda ??? : ???))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort menu using length of item names (keys)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'pie': 3.95,\n",
+       " 'milk': 1.65,\n",
+       " 'donut': 1.25,\n",
+       " 'bread': 5.99,\n",
+       " 'orange': 1.19,\n",
+       " 'muffin': 2.25,\n",
+       " 'cookie': 0.79,\n",
+       " 'broccoli': 4.99}"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict(sorted(menu.items(), key = lambda menu_tuple :len( menu_tuple[0]) ))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort menu using decreasing order of prices - v1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'bread': 5.99,\n",
+       " 'broccoli': 4.99,\n",
+       " 'pie': 3.95,\n",
+       " 'muffin': 2.25,\n",
+       " 'milk': 1.65,\n",
+       " 'donut': 1.25,\n",
+       " 'orange': 1.19,\n",
+       " 'cookie': 0.79}"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict(sorted(menu.items(), key = lambda menu_tuple: menu_tuple[1], reverse = True))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort menu using decreasing order of prices - v2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'bread': 5.99,\n",
+       " 'broccoli': 4.99,\n",
+       " 'pie': 3.95,\n",
+       " 'muffin': 2.25,\n",
+       " 'milk': 1.65,\n",
+       " 'donut': 1.25,\n",
+       " 'orange': 1.19,\n",
+       " 'cookie': 0.79}"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict(sorted(menu.items(), key = lambda menu_tuple: -menu_tuple[1]))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Iterable\n",
+    "\n",
+    "- What is an iterable? Anything that you can write a for loop to iterate over is called as an iterable.\n",
+    "- Examples of iteratables:\n",
+    "    - `list`, `str`, `tuple`, `range()` (any sequence)\n",
+    "    - `dict`"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## List comprehensions\n",
+    "\n",
+    "- concise way of generating a new list based on existing list item manipulation \n",
+    "- short syntax - easier to read, very difficult to debug\n",
+    "\n",
+    "<pre>\n",
+    "new_list = [expression for val in iterable if conditional_expression]\n",
+    "</pre>\n",
+    "- iteratble: reference to any iterable object instance\n",
+    "- conditional_expression: filters the values in the original list based on a specific requirement\n",
+    "- expression: can simply be val or some other transformation of val\n",
+    "- enclosing [ ] represents new list\n",
+    "\n",
+    "Best approach:\n",
+    "- write for clause first\n",
+    "- if condition expression next\n",
+    "- expression in front of for clause last"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Which animals are in all caps?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Original: ['lion', 'badger', 'RHINO', 'GIRAFFE']\n",
+      "New list: ['RHINO', 'GIRAFFE']\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Recap: retain animals in all caps\n",
+    "animals = [\"lion\", \"badger\", \"RHINO\", \"GIRAFFE\"]\n",
+    "caps_animals = []\n",
+    "print(\"Original:\", animals)\n",
+    "\n",
+    "for val in animals:\n",
+    "    if val.upper() == val:\n",
+    "         caps_animals.append(val)  \n",
+    "        \n",
+    "print(\"New list:\", caps_animals)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Now let's solve the same problem using list comprehension\n",
+    "<pre>\n",
+    "new_list = [expression for val in iterable if conditional_expression]\n",
+    "</pre>\n",
+    "For the below example:\n",
+    "- iterable: animals variable (storing reference to a list object instance)\n",
+    "- conditional_expression: val.upper() == val\n",
+    "- expression: val itself"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Original: ['lion', 'badger', 'RHINO', 'GIRAFFE']\n",
+      "New list: ['RHINO', 'GIRAFFE']\n"
+     ]
+    }
+   ],
+   "source": [
+    "# List comprehension version\n",
+    "print(\"Original:\", animals)\n",
+    "\n",
+    "caps_animals = [val for val in animals if val.upper() == val ]\n",
+    "print(\"New list:\", caps_animals)\n",
+    "\n",
+    "# final version to uncomment if you want:\n",
+    "# caps_animals = [val for val in animals if val.upper() == val]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Why is to tougher to debug?\n",
+    "- you cannot use a print function call in a comprehension\n",
+    "- you need to decompose each part and test it separately\n",
+    "- recommended to write the comprehension with a simpler example"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Other than a badger, what animals can you see at Henry Vilas Zoo?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Original: ['lion', 'badger', 'RHINO', 'GIRAFFE']\n",
+      "New list: ['lion', 'RHINO', 'GIRAFFE']\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"Original:\", animals)\n",
+    "\n",
+    "# non_badger_zoo_animals = ???\n",
+    "\n",
+    "# step 1\n",
+    "# non_badger_zoo_animals = [??? for val in animals ???]\n",
+    "                          \n",
+    "# # step 2\n",
+    "# non_badger_zoo_animals = [??? for val in animals if val != \"badger\"]                          \n",
+    "\n",
+    "# # step 3\n",
+    "non_badger_zoo_animals = [val for val in animals if val != \"badger\"]\n",
+    "\n",
+    "print(\"New list:\", non_badger_zoo_animals)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Can we convert all of the animals to all caps?\n",
+    "- if clause is optional"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Original: ['lion', 'badger', 'RHINO', 'GIRAFFE']\n",
+      "New list: ['LION', 'BADGER', 'RHINO', 'GIRAFFE']\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"Original:\", animals)\n",
+    "\n",
+    "all_caps_animals = [ val.upper() for val in animals  ]\n",
+    "print(\"New list:\", all_caps_animals)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Can we generate a list to store length of each animal name?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Original: ['lion', 'badger', 'RHINO', 'GIRAFFE']\n",
+      "New list: [4, 6, 5, 7]\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"Original:\", animals)\n",
+    "\n",
+    "animals_name_length = [ len(val) for val in animals  ]\n",
+    "print(\"New list:\", animals_name_length)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Using if ... else ... in a list comprehension\n",
+    "- syntax changes slightly for if ... else ...\n",
+    "\n",
+    "<pre>\n",
+    "new_list = [expression if conditional_expression else alternate_expression for val in iterable ]\n",
+    "</pre>\n",
+    "\n",
+    "- when an item satifies the if clause, you don't execute the else clause\n",
+    "    - expression is the item in new list when if condition is satified\n",
+    "- when an item does not satisfy the if clause, you execute the else clause\n",
+    "    - alternate_expression is the item in new list when if condition is not satisfied\n",
+    "    \n",
+    "- if ... else ... clauses need to come before for (not the same as just using if clause)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### What if we only care about the badger? Replace non-badger animals with \"some animal\"."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Original: ['lion', 'badger', 'RHINO', 'GIRAFFE']\n",
+      "New list: ['some animal', 'badger', 'some animal', 'some animal']\n"
+     ]
+    }
+   ],
+   "source": [
+    "animals = [\"lion\", \"badger\", \"RHINO\", \"GIRAFFE\"]\n",
+    "print(\"Original:\", animals)\n",
+    "\n",
+    "#non_badger_zoo_animals = ???\n",
+    "\n",
+    "# # step 1:\n",
+    "# non_badger_zoo_animals = [   ???  for val in animals ???]\n",
+    "\n",
+    "# # step 2:\n",
+    "# non_badger_zoo_animals = [ ??? if val == \"badger\" else ??? for val in animals]\n",
+    "\n",
+    "# # step 3: fill in \"val\"\n",
+    " #non_badger_zoo_animals = [val if val == \"badger\" else ?? for val in animals]\n",
+    "\n",
+    "# # step 4: fill in else case\n",
+    "non_badger_zoo_animals = [val if val == \"badger\" else \"some animal\" for val in animals]\n",
+    "\n",
+    "print(\"New list:\", non_badger_zoo_animals)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Dict comprehensions\n",
+    "- Version 1:\n",
+    "<pre>\n",
+    "{expression for val in iterable if condition}\n",
+    "</pre>\n",
+    "- expression has the form <pre>key: val</pre>\n",
+    "<br/>\n",
+    "- Version 2 --- the dict function call by passing list comprehension as argument:\n",
+    "<pre>dict([expression for val in iterable if condition])</pre>\n",
+    "- expression has the form <pre>(key, val)</pre>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Create a dict to map number to its square (for numbers 1 to 5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}\n"
+     ]
+    }
+   ],
+   "source": [
+    "squares_dict = dict()\n",
+    "for val in range(1, 6):\n",
+    "    squares_dict[val] = val * val\n",
+    "print(squares_dict)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Dict comprehension --- version 1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}\n"
+     ]
+    }
+   ],
+   "source": [
+    "square_dict = { i : i * i for i in range(1, 6) }\n",
+    "print(square_dict)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Dict comprehension --- version 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}\n"
+     ]
+    }
+   ],
+   "source": [
+    "square_dict = dict( [ (i, i * i ) for i in range(1,6) ] )\n",
+    "print(square_dict)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Tuple unpacking\n",
+    "- you can directly specific variables to unpack the items inside a tuple"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "('Bob', '32')\n",
+      "('Cindy', '45')\n",
+      "('Alice', '39')\n",
+      "('Unknown', 'None')\n",
+      "--------------------\n",
+      "Bob 32\n",
+      "Cindy 45\n",
+      "Alice 39\n",
+      "Unknown None\n"
+     ]
+    }
+   ],
+   "source": [
+    "scores_dict = {\"Bob\": \"32\", \"Cindy\" : \"45\", \"Alice\": \"39\", \"Unknown\": \"None\"}\n",
+    "\n",
+    "for tuple_item in scores_dict.items():\n",
+    "    print(tuple_item)\n",
+    "    \n",
+    "print(\"--------------------\")\n",
+    "\n",
+    "for key, val in scores_dict.items():\n",
+    "    print(key, val)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### From square_dict, let's generate cube_dict"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}"
+      ]
+     },
+     "execution_count": 24,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "square_dict"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "{1: 1, 2: 8, 3: 27, 4: 64, 5: 125}\n"
+     ]
+    }
+   ],
+   "source": [
+    "cube_dict = {key: int(math.sqrt(val)) ** 3 for key, val in square_dict.items()}\n",
+    "print(cube_dict)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Convert Madison *F temperature to *C\n",
+    "- <pre>C = 5 / 9 * (F - 32)</pre>"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "madison_fahrenheit = {'Nov': 28,'Dec': 20, 'Jan': 10,'Feb': 14}\n",
+    "print(\"Original:\", madison_fahrenheit)\n",
+    "\n",
+    "madison_celsius = {key: ??? \\\n",
+    "                   for key, val in madison_fahrenheit.items()}\n",
+    "print(\"New dict:\", madison_celsius)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Convert type of values in a dictionary"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "scores_dict = {\"Bob\": \"32\", \"Cindy\" : \"45\", \"Alice\": \"39\", \"Unknown\": \"None\"}\n",
+    "print(\"Original:\", scores_dict)\n",
+    "\n",
+    "updated_scores_dict = {???}\n",
+    "                       \n",
+    "# # step 1: add for statement\n",
+    "# updated_scores_dict = { ??? for key, val in scores_dict.items() ???}\n",
+    "\n",
+    "# # step 2: add if statement - but use if/else to handle None values\n",
+    "# updated_scores_dict = { ??? if val.isdigit() else ??? for key, val in scores_dict.items()}\n",
+    "\n",
+    "# # step 3: fill in \"if\" and \"else\" values\n",
+    "# updated_scores_dict = {key: int(val) if val.isdigit() else None \\\n",
+    "#                        for key, val in scores_dict.items()}\n",
+    "\n",
+    "print(\"New dict:\", updated_scores_dict)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Create a dictionary to map each player to their max score"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "scores_dict = {\"Bob\": [18, 72, 61, 5, 83], \n",
+    "               \"Cindy\" : [27, 11, 55, 73, 87], \n",
+    "               \"Alice\": [16, 33, 42, 89, 90], \n",
+    "               \"Meena\": [39, 93, 9, 3, 55]}\n",
+    "\n",
+    "{player: max(scores) for player, scores in scores_dict.items()}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Practice problems - sorted + lambda"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Use sorted and lambda function to sort this list of dictionaries based on the score, from low to high"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "scores = [  {\"name\": \"Bob\", \"score\": 32} ,\n",
+    "            {\"name\": \"Cindy\", \"score\" : 45}, \n",
+    "            {\"name\": \"Alice\", \"score\": 39}\n",
+    "     ]\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Now, modify the lambda function part alone to sort the list of dictionaries based on the score, from high to low"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Now, go back to the previous lambda function definition and use sorted parameters to sort the list of dictionaries based on the score, from high to low"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Student Information Survey dataset analysis"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def median(items):\n",
+    "    items.sort()\n",
+    "    n = len(items)\n",
+    "    if n % 2 != 0:\n",
+    "        middle = items[n // 2]\n",
+    "    else:\n",
+    "        first_middle = items[n // 2]\n",
+    "        second_middle = items[(n // 2) - 1]\n",
+    "        middle = (first_middle + second_middle) / 2\n",
+    "    return middle"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# inspired by https://automatetheboringstuff.com/2e/chapter16/\n",
+    "def process_csv(filename):\n",
+    "    exampleFile = open(filename, encoding=\"utf-8\")  \n",
+    "    exampleReader = csv.reader(exampleFile) \n",
+    "    exampleData = list(exampleReader)        \n",
+    "    exampleFile.close()  \n",
+    "    return exampleData\n",
+    "\n",
+    "survey_data = process_csv('cs220_survey_data.csv')\n",
+    "cs220_header = survey_data[0]\n",
+    "cs220_data = survey_data[1:]\n",
+    "cs220_header"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def cell(row_idx, col_name):\n",
+    "    \"\"\"\n",
+    "    Returns the data value (cell) corresponding to the row index and \n",
+    "    the column name of a CSV file.\n",
+    "    \"\"\"\n",
+    "    col_idx = cs220_header.index(col_name) \n",
+    "    val = cs220_data[row_idx][col_idx]  \n",
+    "    \n",
+    "    # handle missing values\n",
+    "    if val == '':\n",
+    "        return None\n",
+    "    \n",
+    "    # handle type conversions\n",
+    "    if col_name == \"Age\":\n",
+    "        val = int(val)\n",
+    "        if 0 < val <= 118:\n",
+    "            return val\n",
+    "        else:\n",
+    "            # Data cleaning\n",
+    "            return None\n",
+    "    elif col_name in ['Zip Code',]:\n",
+    "        return int(val)\n",
+    "    elif col_name in ['Latitude', 'Longitude']:\n",
+    "        return float(val)\n",
+    "    \n",
+    "    return val"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def transform(header, data):\n",
+    "    \"\"\"\n",
+    "    Transform data into a list of dictionaries, while taking care of type conversions\n",
+    "    \"\"\"\n",
+    "    #should be defined outside the for loop, because it stores the entire data\n",
+    "    dict_list = []     \n",
+    "    for row_idx in range(len(data)):\n",
+    "        row = data[row_idx]\n",
+    "        #should be defined inside the for loop, because it represents one row as a \n",
+    "        #dictionary\n",
+    "        new_row = {}         \n",
+    "        for i in range(len(header)):\n",
+    "            val = cell(row_idx, header[i])\n",
+    "            new_row[header[i]] = val\n",
+    "        dict_list.append(new_row)\n",
+    "    return dict_list\n",
+    "        \n",
+    "transformed_data = transform(cs220_header, cs220_data)\n",
+    "transformed_data[:2] # top 2 rows"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def bucketize(data, bucket_column):\n",
+    "    \"\"\"\n",
+    "    data: expects list of dictionaries\n",
+    "    bucket_column: column for bucketization\n",
+    "    generates and returns bucketized data based on bucket_column\n",
+    "    \"\"\"\n",
+    "    # Key: unique bucketize column value; Value: list of dictionaries \n",
+    "    # (rows having that unique column value)\n",
+    "    buckets = dict()\n",
+    "    for row_dict in data:\n",
+    "        col_value = row_dict[bucket_column]\n",
+    "        if col_value not in buckets:\n",
+    "            buckets[col_value] = []\n",
+    "        buckets[col_value].append(row_dict)\n",
+    "        \n",
+    "    return buckets"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### What is the average age of \"LEC001\" students?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "lecture_buckets = bucketize(transformed_data, \"Lecture\")\n",
+    "lec001_bucket = lecture_buckets[\"LEC001\"]\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### What is the average age of \"LEC001\" students who like \"pineapple\" pizza topping?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### What are the sleep habits of the youngest students?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "min_age = None\n",
+    "\n",
+    "# pass 1: find minimum age\n",
+    "\n",
+    "\n",
+    "# pass 2: find sleep habit of students with minimum age\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### How many students are there is each lecture?\n",
+    "- Create a `dict` mapping each lecture to the count of students."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# v1\n",
+    "{}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# v2\n",
+    "{}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Find whether 15 oldest students in the class are runners?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "students_with_age = []"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Compute median age per lecture in one step using `dict` and `list` comprehension."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "age_by_lecture = {} # Key: lecture; Value: list of ages\n",
+    "\n",
+    "for lecture in lecture_buckets:\n",
+    "    lecture_students = lecture_buckets[lecture]\n",
+    "    ages = []\n",
+    "    for student in lecture_students:\n",
+    "        age = student[\"Age\"]\n",
+    "        if age == None:\n",
+    "            continue\n",
+    "        ages.append(age)\n",
+    "    age_by_lecture[lecture] = ages\n",
+    "\n",
+    "median_age_by_lecture = {} # Key: lecture; Value: median age of that lecture\n",
+    "for lecture in age_by_lecture:\n",
+    "    median_age = median(age_by_lecture[lecture])\n",
+    "    median_age_by_lecture[lecture] = median_age\n",
+    "    \n",
+    "print(median_age_by_lecture)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Compute max age per lecture in one step using `dict` and `list` comprehension."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Practice problems - comprehensions"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Generate a new list where each number is a square of the original nummber in numbers list"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "numbers = [44, 33, 56, 21, 19]\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Generate a new list of floats from vac_rates, that is rounded to 3 decimal points"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "vac_rates = [23.329868, 51.28772, 76.12232, 17.2, 10.5]\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Generate a new list of ints from words, that contains length of each word"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "words = ['My', 'very', 'educated', 'mother', 'just', 'served', 'us', 'noodles']\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Create 2 dictionaries to map each player to their min and avg score"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "scores_dict = {\"Bob\": [18, 72, 61, 5, 83], \n",
+    "               \"Cindy\" : [27, 11, 55, 73, 87], \n",
+    "               \"Alice\": [16, 33, 42, 89, 90], \n",
+    "               \"Meena\": [39, 93, 9, 3, 55]}\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Student Information Survey dataset"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Create dict mapping unique age to count of students with that age.\n",
+    "- Order the dictionary based on increasing order of ages\n",
+    "- Make sure to drop student dictionaries which don't have Age column information (we already did this in a previous example)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Find whether 15 youngest students in the class are pet owners?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/sum23/lecture_materials/14_Func_Refs/lec_14_function_references_notes.ipynb b/sum23/lecture_materials/14_Func_Refs/lec_14_function_references_notes.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..63f982d5d0b5fb2216f4b6a205dce2226e3522c9
--- /dev/null
+++ b/sum23/lecture_materials/14_Func_Refs/lec_14_function_references_notes.ipynb
@@ -0,0 +1,1420 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Function references"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Recursion review"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Nested data structures are defined recursively.\n",
+    "\n",
+    "# A Python list can contain lists\n",
+    "# A Python dictionary can contain dictionaries\n",
+    "# A JSON dictionary can contain a JSON dictionary"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Trace Recursion by hand\n",
+    "# Run this on your own in Python Tutor\n",
+    "\n",
+    "def mystery(a, b): \n",
+    "    # precondition: assume a > 0 and b > 0\n",
+    "    if b == 1: \n",
+    "        return a\n",
+    "    return a * mystery(a, b - 1)\n",
+    "\n",
+    "# make a function call here\n",
+    "mystery(3, 2)\n",
+    "\n",
+    "# TODO: what does the mystery function compute?\n",
+    "\n",
+    "# Question: What would be the result of the below function call?\n",
+    "# mystery(-3, -1) \n",
+    "# Answer: "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Learning Objectives:\n",
+    "\n",
+    "- Define a function reference and trace code that uses function references.\n",
+    "- Explain the default use of `sorted()` on lists of tuples, and dictionaries.\n",
+    "- Sort a list of tuples, a list of dictionaries, or a dictionary using a function as a key.\n",
+    "- Use a lambda expression when sorting."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Functions are objects\n",
+    "\n",
+    "- Every data in Python is an object instance, including a function definition\n",
+    "- Implications:\n",
+    "    - variables can reference functions\n",
+    "    - lists/dicts can reference functions\n",
+    "    - we can pass function references to other functions\n",
+    "    - we can pass lists of function references to other functions"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 1: function object references\n",
+    "#### Use PyTutor to step through this example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "3\n"
+     ]
+    }
+   ],
+   "source": [
+    "l1 = [1, 2, 3]    # Explanation: l1 should reference a new list object\n",
+    "l2 = l1           # Explanation: l2 should reference whatever l1 references\n",
+    "\n",
+    "def f(l):         # Explanation: f should reference a new function object\n",
+    "    return l[-1]\n",
+    "\n",
+    "g = f             # Explanation: g should reference whatever f references\n",
+    "\n",
+    "num = f(l2)       # Explanation: l should reference whatever l2 references\n",
+    "                  # Explanation: num should reference whatever f returns\n",
+    "\n",
+    "print(num)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Function references\n",
+    "\n",
+    "- Since function definitions are objects in Python, function reference is a variable that refers to a function object.\n",
+    "- In essence, it gives a function another name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "3\n",
+      "3\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Both these calls would have run the same code, returning the same result\n",
+    "num = f(l1)\n",
+    "print(num)\n",
+    "num = g(l2) \n",
+    "print(num)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 2: function references can be passed as arguments to another function, wow!\n",
+    "#### Use PyTutor to step through this example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello there!\n",
+      "Hello there!\n",
+      "Wash your hands and stay well, bye!\n",
+      "Wash your hands and stay well, bye!\n",
+      "Wash your hands and stay well, bye!\n"
+     ]
+    }
+   ],
+   "source": [
+    "def say_hi():\n",
+    "    print(\"Hello there!\")\n",
+    "\n",
+    "def say_bye():\n",
+    "    print(\"Wash your hands and stay well, bye!\")\n",
+    "    \n",
+    "f = say_hi\n",
+    "f()\n",
+    "f()\n",
+    "f = say_bye\n",
+    "f()\n",
+    "f()\n",
+    "f()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello there!\n",
+      "Hello there!\n",
+      "Wash your hands and stay well, bye!\n",
+      "Wash your hands and stay well, bye!\n",
+      "Wash your hands and stay well, bye!\n"
+     ]
+    }
+   ],
+   "source": [
+    "for i in range(2):\n",
+    "    say_hi()\n",
+    "\n",
+    "for i in range(3):\n",
+    "    say_bye()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello there!\n",
+      "Hello there!\n",
+      "Wash your hands and stay well, bye!\n",
+      "Wash your hands and stay well, bye!\n",
+      "Wash your hands and stay well, bye!\n"
+     ]
+    }
+   ],
+   "source": [
+    "def call_n_times(f, n):\n",
+    "    for i in range(n):\n",
+    "        f()\n",
+    "\n",
+    "call_n_times(say_hi, 2)\n",
+    "call_n_times(say_bye, 3)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Wash your hands and stay well, bye!\n"
+     ]
+    },
+    {
+     "ename": "TypeError",
+     "evalue": "'NoneType' object is not callable",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
+      "Cell \u001b[0;32mIn[7], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mcall_n_times\u001b[49m\u001b[43m(\u001b[49m\u001b[43msay_bye\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# uncomment to see TypeError\u001b[39;00m\n\u001b[1;32m      3\u001b[0m \u001b[38;5;66;03m# Question: Why does this give TypeError?\u001b[39;00m\n\u001b[1;32m      4\u001b[0m \u001b[38;5;66;03m# Answer: when you specify say_bye(), you are invoking the function, which returns None\u001b[39;00m\n\u001b[1;32m      5\u001b[0m \u001b[38;5;66;03m#         (default return value when return statement is not defined)\u001b[39;00m\n",
+      "Cell \u001b[0;32mIn[6], line 3\u001b[0m, in \u001b[0;36mcall_n_times\u001b[0;34m(f, n)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcall_n_times\u001b[39m(f, n):\n\u001b[1;32m      2\u001b[0m     \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(n):\n\u001b[0;32m----> 3\u001b[0m         \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
+      "\u001b[0;31mTypeError\u001b[0m: 'NoneType' object is not callable"
+     ]
+    }
+   ],
+   "source": [
+    "call_n_times(say_bye(), 3) # uncomment to see TypeError\n",
+    "\n",
+    "# Question: Why does this give TypeError?\n",
+    "# Answer: when you specify say_bye(), you are invoking the function, which returns None\n",
+    "#         (default return value when return statement is not defined)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 3: Apply various transformations to all items on a list"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "L = [\"1\", \"23\", \"456\"]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Write apply_to_each function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# a. Input: list object reference, function object\n",
+    "# b. Output: new list reference to transformed object\n",
+    "# c. Pseudocode:\n",
+    "#        1. Initiliaze new empty list for output - we don't want to modify \n",
+    "#           the input list!      \n",
+    "#        2. Process each item in input list\n",
+    "#        3. Apply the function passed as arugment to 2nd parameter\n",
+    "#        4. And the transformed item into output list\n",
+    "#        5. return output list\n",
+    "\n",
+    "def apply_to_each(original_L, f):\n",
+    "    \"\"\"\n",
+    "    returns a new list with transformed items, by applying f function\n",
+    "    to each item in the original list\n",
+    "    \"\"\"\n",
+    "\n",
+    "    # step 1: create a new list\n",
+    "    new_vals = []\n",
+    "\n",
+    "    # step 2: iterate through items in original_L\n",
+    "    for val in original_L:\n",
+    "        # step 3: apply f to each item\n",
+    "        new_vals.append( f(val) )\n",
+    "    \n",
+    "    # step 4: return new list\n",
+    "    return new_vals \n",
+    "    pass"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Apply `int` function to list L using apply_to_each function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "34"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# what does int do?\n",
+    "int(\"34\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[1, 23, 456]"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "apply_to_each(L, int)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Write strip_dollar function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# a. Input: string value\n",
+    "# b. Output: transformed string value\n",
+    "# c. Pseudocode: \n",
+    "#       1. Check whether input string begins with $ - \n",
+    "#          what string method do you need here?\n",
+    "#        2. If so remove it\n",
+    "\n",
+    "def strip_dollar(s):\n",
+    "    \"\"\"\n",
+    "    Removes the beginning $ sign from string s\n",
+    "    \"\"\"\n",
+    "\n",
+    "    # Step 1: check whether input string begins with $\n",
+    "    if s.startswith(\"$\"):\n",
+    "        # Step 2: if so, remove it\n",
+    "        s = s[1:]\n",
+    "    \n",
+    "    # Step 3: return the new string\n",
+    "    return s"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Apply strip_dollar function and then apply int function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['1', '23', '456']\n",
+      "[1, 23, 456]\n"
+     ]
+    }
+   ],
+   "source": [
+    "L = [\"$1\", \"23\", \"$456\"]\n",
+    "vals = apply_to_each(L, strip_dollar)\n",
+    "print(vals)\n",
+    "vals = apply_to_each(vals, int)\n",
+    "print(vals)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Apply upper method call to the below list L by using apply_to_each function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['AAA', 'BBB', 'CCC']\n"
+     ]
+    }
+   ],
+   "source": [
+    "L = [\"aaa\", \"bbb\", \"ccc\"]\n",
+    "vals = apply_to_each(L, str.upper)\n",
+    "print(vals)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'AAA'"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# remember, these are equivalent:\n",
+    "\"aaa\".upper()\n",
+    "str.upper(\"aaa\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Custom sorting nested data structures\n",
+    "\n",
+    "Examples:\n",
+    "- list of tuples\n",
+    "- list of dictionaries"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 4: Custom sort a list of tuples"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('JJ', 'Watt', 31),\n",
+       " ('Jonathan', 'Taylor', 22),\n",
+       " ('Melvin', 'Gordon', 27),\n",
+       " ('Russel', 'Wilson', 32),\n",
+       " ('Troy', 'Fumagalli', 88)]"
+      ]
+     },
+     "execution_count": 17,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "badgers_in_nfl = [ # tuple storing (first name, last name, age)\n",
+    "                   (\"Jonathan\", \"Taylor\", 22 ), \n",
+    "                   (\"Russel\", \"Wilson\", 32), \n",
+    "                   (\"Troy\", \"Fumagalli\", 88),\n",
+    "                   (\"Melvin\", \"Gordon\", 27), \n",
+    "                   (\"JJ\", \"Watt\", 31),\n",
+    "                 ]\n",
+    "\n",
+    "sorted(badgers_in_nfl) # or sort() method by default uses first element to sort"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### What what if we want to sort by the last name or by the length of the name?\n",
+    "\n",
+    "- `sorted` function and `sort` method takes a function reference as keyword argument for the parameter `key`\n",
+    "- We can define functions that take one of the inner data structure as argument and return the field based on which we want to perform the sorting.\n",
+    "    - We then pass a reference to such a function as argument to the parameter `key`.\n",
+    "    \n",
+    "#### Define functions that will enable extraction of item at each tuple index position. These functions only deal with a single tuple processing"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def extract_fname(player_tuple):  # function must have exactly one parameter\n",
+    "    return player_tuple[0]\n",
+    "\n",
+    "def extract_lname(player_tuple):\n",
+    "    return player_tuple[1]\n",
+    "\n",
+    "def extract_age(player_tuple):\n",
+    "    return player_tuple[2]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'JJ'"
+      ]
+     },
+     "execution_count": 20,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Test extract_fname function on the tuple ('JJ', 'Watt', 31)\n",
+    "extract_fname(('JJ', 'Watt', 31))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players by their last name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('Troy', 'Fumagalli', 88),\n",
+       " ('Melvin', 'Gordon', 27),\n",
+       " ('Jonathan', 'Taylor', 22),\n",
+       " ('JJ', 'Watt', 31),\n",
+       " ('Russel', 'Wilson', 32)]"
+      ]
+     },
+     "execution_count": 21,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted(badgers_in_nfl, key = extract_lname) "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players by their age"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('Jonathan', 'Taylor', 22),\n",
+       " ('Melvin', 'Gordon', 27),\n",
+       " ('JJ', 'Watt', 31),\n",
+       " ('Russel', 'Wilson', 32),\n",
+       " ('Troy', 'Fumagalli', 88)]"
+      ]
+     },
+     "execution_count": 22,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted(badgers_in_nfl, key = extract_age) "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players by descending order of age"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('Troy', 'Fumagalli', 88),\n",
+       " ('Russel', 'Wilson', 32),\n",
+       " ('JJ', 'Watt', 31),\n",
+       " ('Melvin', 'Gordon', 27),\n",
+       " ('Jonathan', 'Taylor', 22)]"
+      ]
+     },
+     "execution_count": 23,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted(badgers_in_nfl, key = extract_age, reverse = True) "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players by length of first name + length of last name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[('JJ', 'Watt', 31),\n",
+       " ('Russel', 'Wilson', 32),\n",
+       " ('Melvin', 'Gordon', 27),\n",
+       " ('Troy', 'Fumagalli', 88),\n",
+       " ('Jonathan', 'Taylor', 22)]"
+      ]
+     },
+     "execution_count": 25,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def compute_name_length(player_tuple):\n",
+    "    first_name = extract_fname(player_tuple)\n",
+    "    last_name = extract_lname(player_tuple)\n",
+    "    return len(first_name) + len(last_name)\n",
+    "\n",
+    "sorted(badgers_in_nfl, key = compute_name_length) "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 5: Custom sort a list of dictionaries"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "hurricanes = [\n",
+    "    {\"name\": \"A\", \"year\": 2000, \"speed\": 150},\n",
+    "    {\"name\": \"B\", \"year\": 1980, \"speed\": 100},\n",
+    "    {\"name\": \"C\", \"year\": 1990, \"speed\": 250},\n",
+    "]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Extract hurricane at index 0"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'name': 'A', 'year': 2000, 'speed': 150}"
+      ]
+     },
+     "execution_count": 27,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hurricanes[0]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Extract hurricane at index 1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'name': 'B', 'year': 1980, 'speed': 100}"
+      ]
+     },
+     "execution_count": 28,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hurricanes[1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Can you compare hurricane at index 0 and hurricane at index 1 using \"<\" operator?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "metadata": {},
+   "outputs": [
+    {
+     "ename": "TypeError",
+     "evalue": "'<' not supported between instances of 'dict' and 'dict'",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
+      "Cell \u001b[0;32mIn[29], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mhurricanes\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m<\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mhurricanes\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;66;03m#uncomment to see TypeError\u001b[39;00m\n",
+      "\u001b[0;31mTypeError\u001b[0m: '<' not supported between instances of 'dict' and 'dict'"
+     ]
+    }
+   ],
+   "source": [
+    "hurricanes[0] < hurricanes[1] #uncomment to see TypeError"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### What about calling sorted method by passing hurricanes as argument?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "metadata": {},
+   "outputs": [
+    {
+     "ename": "TypeError",
+     "evalue": "'<' not supported between instances of 'dict' and 'dict'",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
+      "Cell \u001b[0;32mIn[30], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43msorted\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mhurricanes\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# Doesn't work because there isn't a defined \"first\" key in a dict.\u001b[39;00m\n\u001b[1;32m      2\u001b[0m \u001b[38;5;66;03m# Unlike tuple, where the first item can be considered \"first\" by ordering.\u001b[39;00m\n",
+      "\u001b[0;31mTypeError\u001b[0m: '<' not supported between instances of 'dict' and 'dict'"
+     ]
+    }
+   ],
+   "source": [
+    "sorted(hurricanes) # Doesn't work because there isn't a defined \"first\" key in a dict.\n",
+    "# Unlike tuple, where the first item can be considered \"first\" by ordering."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort hurricanes based on the year"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[{'name': 'B', 'year': 1980, 'speed': 100},\n",
+       " {'name': 'C', 'year': 1990, 'speed': 250},\n",
+       " {'name': 'A', 'year': 2000, 'speed': 150}]"
+      ]
+     },
+     "execution_count": 31,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# a. Input: single hurricane's dict\n",
+    "# b. Output: return \"year\" value from the dict\n",
+    "\n",
+    "def get_year(hurricane_dict):\n",
+    "    return hurricane_dict[\"year\"]\n",
+    "\n",
+    "sorted(hurricanes, key = get_year)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort hurricanes in descending order of their year"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[{'name': 'A', 'year': 2000, 'speed': 150},\n",
+       " {'name': 'C', 'year': 1990, 'speed': 250},\n",
+       " {'name': 'B', 'year': 1980, 'speed': 100}]"
+      ]
+     },
+     "execution_count": 32,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted(hurricanes, key = get_year, reverse = True) \n",
+    "# alternatively get_year function could return negative of year \n",
+    "# --- that produces the same result as passing True as argument to reverse parameter"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort hurricanes in ascending order of their speed"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 34,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[{'name': 'C', 'year': 1990},\n",
+       " {'name': 'B', 'year': 1980, 'speed': 100},\n",
+       " {'name': 'A', 'year': 2000, 'speed': 150}]"
+      ]
+     },
+     "execution_count": 34,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hurricanes = [\n",
+    "    {\"name\": \"A\", \"year\": 2000, \"speed\": 150},\n",
+    "    {\"name\": \"B\", \"year\": 1980, \"speed\": 100},\n",
+    "    {\"name\": \"C\", \"year\": 1990}, # notice the missing speed key\n",
+    "]\n",
+    "\n",
+    "def get_speed(hurricane):\n",
+    "    if not \"speed\" in hurricane:\n",
+    "        return 0\n",
+    "    return hurricane[\"speed\"]\n",
+    "\n",
+    "sorted(hurricanes, key = get_speed)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 6: How can you pass string method to sorted function?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 36,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['A', 'C', 'b', 'd', 'e']"
+      ]
+     },
+     "execution_count": 36,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted([\"A\", \"b\", \"e\", \"C\", \"d\"])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 37,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['A', 'b', 'C', 'd', 'e']"
+      ]
+     },
+     "execution_count": 37,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted([\"A\", \"b\", \"e\", \"C\", \"d\"], key = str.upper) \n",
+    "# hint: to capitalize \"hello\", we call \"hello\".upper() or str.upper(\"hello\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Sorting dictionary by keys / values"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Example 7: sorting dictionaries"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 38,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'bob': 20, 'alice': 8, 'alex': 9, 'cindy': 15}"
+      ]
+     },
+     "execution_count": 38,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "players = {\n",
+    "    \"bob\": 20, \n",
+    "    \"alice\": 8, \n",
+    "    \"alex\": 9, \n",
+    "    \"cindy\": 15} # Key: player_name; Value: score\n",
+    "players"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### This only returns a list of sorted keys. What if we want to create a new sorted dictionary object directly using sorted function?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 39,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['alex', 'alice', 'bob', 'cindy']"
+      ]
+     },
+     "execution_count": 39,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sorted(players) "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Let's learn about items method on a dictionary\n",
+    "- returns a list of tuples\n",
+    "- each tuple item contains two items: key and value"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 40,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "dict_items([('bob', 20), ('alice', 8), ('alex', 9), ('cindy', 15)])"
+      ]
+     },
+     "execution_count": 40,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "players.items()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Write an extract function to extract dict value (that is player score), using items method return value"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 45,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def extract_score(player_tuple):\n",
+    "    print(player_tuple)\n",
+    "    return player_tuple[1]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# FYI, lambda version\n",
+    "dict( sorted(players.items(), key = lambda item: item[1] ))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Sort players dict by key"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 47,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "('bob', 20)\n",
+      "('alice', 8)\n",
+      "('alex', 9)\n",
+      "('cindy', 15)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "[('alice', 8), ('alex', 9), ('cindy', 15), ('bob', 20)]"
+      ]
+     },
+     "execution_count": 47,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# we'll walk through this step-by-step\n",
+    "# step1: \n",
+    "# sorted(???, key = ???)\n",
+    "\n",
+    "# step 2: fill in blanks\n",
+    "# sorted(players, key = extract_score)  # --> uncomment!\n",
+    "\n",
+    "# step 3: evaluate the input, is it right?\n",
+    "# no -- what's happening? see next cell\n",
+    "# We want the input to extract_score to be a tuple, not a key\n",
+    "\n",
+    "# fix the code\n",
+    "sorted(players.items(), key = extract_score) # --> uncomment"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 44,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "o\n",
+      "l\n",
+      "l\n",
+      "i\n"
+     ]
+    }
+   ],
+   "source": [
+    "# when we call sorted(players, key = extract_score), this is what gets compared under the hood:\n",
+    "for item in players:\n",
+    "    print(item[1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "How can you convert sorted list of tuples back into a `dict`?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 50,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "('bob', 20)\n",
+      "('alice', 8)\n",
+      "('alex', 9)\n",
+      "('cindy', 15)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "{'alice': 8, 'alex': 9, 'cindy': 15, 'bob': 20}"
+      ]
+     },
+     "execution_count": 50,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict (sorted(players.items(), key = extract_score)) # --> uncomment"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Using `lambda`\n",
+    "- `lambda` functions are a way to abstract a function reference\n",
+    "- lambdas are simple functions with:\n",
+    "    - multiple possible parameters\n",
+    "    - single expression line as the function body\n",
+    "- lambdas are useful abstractions for:\n",
+    "    - mathematical functions\n",
+    "    - lookup operations\n",
+    "- lambdas are often associated with a collection of values within a list\n",
+    "- Syntax: \n",
+    "```python \n",
+    "lambda parameters: expression\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Now let's write the same solution using lambda."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 51,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'alice': 8, 'alex': 9, 'cindy': 15, 'bob': 20}"
+      ]
+     },
+     "execution_count": 51,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict( sorted(players.items(), key = lambda item: item[1] ))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### What about sorting dictionary by values using lambda?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# same as previous example\n",
+    "dict(sorted(players.items(), key = ???))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Now let's sort players dict using length of player name."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 52,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'bob': 20, 'alex': 9, 'alice': 8, 'cindy': 15}"
+      ]
+     },
+     "execution_count": 52,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dict(sorted(players.items(), key = lambda item : len(item[0])))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Self-practice: Use lambdas to solve the NFL sorting questions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(badgers_in_nfl)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players using their first name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players using their last name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players using their age"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Sort players using the length of first name and last name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}