{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e36c5842",
   "metadata": {},
   "source": [
    "# Tuples"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 128,
   "id": "7f2a0d34",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "# Course setup header — run this cell before the others\n",
    "\n",
    "import sys\n",
    "from pathlib import Path\n",
    "\n",
    "current = Path.cwd()\n",
    "for parent in [current, *current.parents]:\n",
    "    if (parent / '_config.yml').exists():\n",
    "        project_root = parent  # ← Add project root, not chapters\n",
    "        break\n",
    "else:\n",
    "    project_root = Path.cwd().parent.parent\n",
    "\n",
    "sys.path.insert(0, str(project_root))\n",
    "\n",
    "\n",
    "from shared import thinkpython, diagram, jupyturtle, download, structshape\n",
    "\n",
    "# Register as top-level modules so direct imports work in subsequent cells\n",
    "sys.modules['thinkpython'] = thinkpython\n",
    "sys.modules['diagram'] = diagram\n",
    "sys.modules['jupyturtle'] = jupyturtle\n",
    "sys.modules['download'] = download\n",
    "sys.modules['structshape'] = structshape\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2434e1a",
   "metadata": {},
   "source": [
    "This chapter introduces tuples, a built-in **sequence** type, and shows how tuples work alongside lists and dictionaries.\n",
    "You will practice tuple assignment and the packing and unpacking operators used with variable-length argument lists.\n",
    "\n",
    "By the end of this chapter, you will be able to:\n",
    "\n",
    "1. Create tuples using literals, packing, and the tuple() constructor\n",
    "2. Unpack tuples (including starred unpacking) in assignments and loops\n",
    "3. Use zip(), enumerate(), divmod(), and other tuple-aware built-ins\n",
    "4. Choose between tuples and lists based on mutability needs\n",
    "5. Return multiple values from a function using tuples\n",
    "\n",
    "One note: There are two common pronunciations of \"tuple\".\n",
    "Some people say \"tuh-ple\" (rhymes with \"supple\").\n",
    "In programming, many people say \"too-ple\" (rhymes with \"quadruple\")."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97514024",
   "metadata": {},
   "source": [
    "## What is a Tuple?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e26428d",
   "metadata": {},
   "source": [
    "- A tuple is a **sequence** of values. \n",
    "- The values can be **any type**. \n",
    "- The values in a tuple are **indexed** by integers, so tuples feel a lot like lists.\n",
    "- When a **function** **returns** multiple values, it returns a tuple. \n",
    "- Tuples are **hashable** when all of their elements are hashable.\n",
    "\n",
    "Tuples are commonly used to group related values together. For example, a latitude and longitude pair naturally belongs together:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "2f0a602e",
   "metadata": {},
   "outputs": [],
   "source": [
    "location = (42.3601, -71.0589)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07a05aab",
   "metadata": {},
   "source": [
    "A tuple is a sequence, just like list. When the data belongs together and shouldn't change - like a coordinate, a date, or a name/age pair - use a tuple. If you need to add, remove, or modify items, use a list.\n",
    "| Feature    | Tuple          | List           |\n",
    "|------------|----------------|----------------|\n",
    "| Syntax     | `(1, 2, 3)`    | `[1, 2, 3]`    |\n",
    "| Mutable    | No             | Yes            |\n",
    "| Hashable   | Yes (conditional) | No          |\n",
    "| Methods    | 2 (`.count`, `.index`) | Many  |\n",
    "| Use case   | Fixed data, records | Collections that change |\n",
    "| Performance | Faster        | Slower         |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf60e652",
   "metadata": {},
   "source": [
    "### Immutability\n",
    "\n",
    "Tuples are **immutable**, which is the key characteristic of tuples. Once created, items cannot be added, removed, or changed within a tuple, but new tuples can be created as a result of operations. That's why tuples lack list methods that modify in place, like `append` and `remove`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b95bc71d",
   "metadata": {},
   "source": [
    "If you try to modify a tuple with indexing, Python raises a `TypeError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca1483cf",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Invoice key[0]: INV-2026-0142\n"
     ]
    },
    {
     "ename": "TypeError",
     "evalue": "'tuple' object does not support item assignment",
     "output_type": "error",
     "traceback": [
      "\u001b[31mTypeError\u001b[39m\u001b[31m:\u001b[39m 'tuple' object does not support item assignment\n"
     ]
    }
   ],
   "source": [
    "%%expect TypeError\n",
    "\n",
    "invoice_key = (\"INV-2026-0142\", \"ACME\", \"2026-02-28\")\n",
    "print(\"Invoice key[0]:\", invoice_key[0])\n",
    "\n",
    "invoice_key[0] = \"INV-2026-0143\" # Tuple does not support item assignment"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09472fdb",
   "metadata": {},
   "source": [
    "## Creating Tuples"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d4232ac",
   "metadata": {},
   "source": [
    "\n",
    "Tuples in Python can be created in several ways, primarily: \n",
    "- Using tuple literals: through the use of commas and optional parentheses\n",
    "- by using the built-in tuple() constructor\n",
    "\n",
    "| How to create | Method | Example | Result |\n",
    "|---|---|---|---|\n",
    "| Use parentheses with comma-separated values | Parentheses | `(1, 2, 3)` | `(1, 2, 3)` |\n",
    "| Write comma-separated values without parentheses | Tuple packing | `1, 2, 3` | `(1, 2, 3)` |\n",
    "| Use empty parentheses | Empty tuple | `()` | `()` |\n",
    "| Add a trailing comma for one value | Single-item tuple | `(1,)` or `1,` | `(1,)` |\n",
    "| Use the tuple constructor on an iterable | Constructor | `tuple([1, 2, 3])` | `(1, 2, 3)` |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e00a9e2",
   "metadata": {},
   "source": [
    "### Tuple Literals\n",
    "\n",
    "To create a tuple, you can write a comma-separated list of values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4fbf044",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tuple"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "status = 'NEW', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CLOSED'\n",
    "type(status)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd01df23",
   "metadata": {},
   "source": [
    "Parentheses are optional, but it is common to use them with tuples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 132,
   "id": "facc9296",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('NEW', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CLOSED')\n",
      "<class 'tuple'>\n",
      "('NEW', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CLOSED')\n",
      "<class 'tuple'>\n"
     ]
    }
   ],
   "source": [
    "t1 = ('NEW', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CLOSED')\n",
    "print(t1)\n",
    "print(type(t1))\n",
    "\n",
    "t2 = 'NEW', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CLOSED'  # tuple packing\n",
    "print(t2)\n",
    "print(type(t2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a55573b5",
   "metadata": {},
   "source": [
    "### Single-Element Tuples"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "601ec2fb",
   "metadata": {},
   "source": [
    "To make a one-element tuple, include a trailing **comma**. Parentheses are optional."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 133,
   "id": "c9e90297",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'tuple'>\n",
      "URGENT\n",
      "<class 'tuple'>\n",
      "URGENT\n"
     ]
    }
   ],
   "source": [
    "t1 = ('URGENT',)\n",
    "print(type(t1))\n",
    "print(t1[0])\n",
    "\n",
    "t2 = 'URGENT',               # tuple packing with one element\n",
    "print(type(t2))\n",
    "print(t2[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d5feda0",
   "metadata": {},
   "source": [
    "However, a single value inside parentheses is not a tuple without a comma."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b2405131",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "str"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t2 = ('URGENT')\n",
    "type(t2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73c45f96",
   "metadata": {},
   "source": [
    "### `tuple()` Constructor\n",
    "\n",
    "Another way to create a tuple is with the built-in function/type constructor `tuple`. With no argument, it creates an empty tuple."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09065ed6",
   "metadata": {},
   "source": [
    "If the argument is a sequence (list, tuple, or string), the `tuple` constructor returns a tuple containing the elements of that sequence — and `list` works the same way in reverse."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 152,
   "id": "7aa729ac",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('Laptop', 'Mouse', 'Keyboard', 'Monitor')\n",
      "<class 'tuple'>\n",
      "['Laptop', 'Mouse', 'Keyboard', 'Monitor']\n",
      "<class 'list'>\n"
     ]
    }
   ],
   "source": [
    "t = tuple(['Laptop', 'Mouse', 'Keyboard', 'Monitor'])\n",
    "print(t)\n",
    "print(type(t))\n",
    "\n",
    "l = list(('Laptop', 'Mouse', 'Keyboard', 'Monitor'))\n",
    "print(l)\n",
    "print(type(l))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40955a1b",
   "metadata": {},
   "source": [
    "### Empty Tuple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "6e77fd12",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "()\n",
      "<class 'tuple'>\n"
     ]
    }
   ],
   "source": [
    "t = tuple()\n",
    "\n",
    "print(t)\n",
    "print(type(t))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08a67131",
   "metadata": {},
   "source": [
    "### Nested Tuples\n",
    "\n",
    "A **nested tuple** is a tuple that contains one or more tuples as elements. This is useful for representing multi-dimensional or grouped data, such as a matrix or a list of coordinate pairs. You access elements using chained indexing: the first index selects the inner tuple, and the second index selects an element within it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "d9d799ab",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(4, 5, 6)\n",
      "6\n",
      "x=0, y=0\n",
      "x=1, y=2\n",
      "x=3, y=4\n"
     ]
    }
   ],
   "source": [
    "# A tuple containing other tuples\n",
    "matrix = ((1, 2, 3), (4, 5, 6), (7, 8, 9))\n",
    "\n",
    "# Access the second row\n",
    "print(matrix[1])        # (4, 5, 6)\n",
    "\n",
    "# Access the third element of the second row\n",
    "print(matrix[1][2])     # 6\n",
    "\n",
    "# Tuple of coordinate pairs\n",
    "points = ((0, 0), (1, 2), (3, 4))\n",
    "for x, y in points:\n",
    "    print(f'x={x}, y={y}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "3308eb8f",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Creating Tuples\n",
    "#   1. Create a tuple named `colors` containing \"red\", \"green\", \"blue\" using parentheses.\n",
    "#   2. Create a single-element tuple named `one` containing the integer 42.\n",
    "#   3. Create a tuple named `letters` by passing the string \"hello\" to the tuple() constructor.\n",
    "#   4. Print each tuple and its type.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "19a73617",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('red', 'green', 'blue') <class 'tuple'>\n",
      "(42,) <class 'tuple'>\n",
      "('h', 'e', 'l', 'l', 'o') <class 'tuple'>\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "colors = (\"red\", \"green\", \"blue\")\n",
    "one = (42,)\n",
    "letters = tuple(\"hello\")\n",
    "\n",
    "print(colors, type(colors))\n",
    "print(one, type(one))\n",
    "print(letters, type(letters))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea5f8ebe",
   "metadata": {},
   "source": [
    "## Accessing Elements"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce1e4032",
   "metadata": {},
   "source": [
    "### Indexing and Slicing\n",
    "\n",
    "Tuples support indexing and slicing the same way lists do."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 153,
   "id": "e6acd9a0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('Mon', 'Tue', 'Wed', 'Thu', 'Fri')\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Mon'"
      ]
     },
     "execution_count": 153,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri')\n",
    "print(t)\n",
    "t[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "714460b0",
   "metadata": {},
   "source": [
    "The slice operator selects a range of elements."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 154,
   "id": "66e9caad",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Tue', 'Wed')"
      ]
     },
     "execution_count": 154,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t[1:3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "bbd11359",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Indexing and Slicing\n",
    "#   Given the tuple: t = ('a', 'b', 'c', 'd', 'e')\n",
    "#   1. Print the first and last elements using indexing.\n",
    "#   2. Print the middle three elements using slicing.\n",
    "#   3. Print the tuple in reverse using a slice.\n",
    "### Your code starts here.\n",
    "\n",
    "t = ('a', 'b', 'c', 'd', 'e')\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "390f1a74",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a e\n",
      "('b', 'c', 'd')\n",
      "('e', 'd', 'c', 'b', 'a')\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "t = ('a', 'b', 'c', 'd', 'e')\n",
    "\n",
    "print(t[0], t[-1])    # first and last\n",
    "print(t[1:4])          # middle three\n",
    "print(t[::-1])         # reversed"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f246a4c3",
   "metadata": {},
   "source": [
    "## Tuple Unpacking"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cc0fd06",
   "metadata": {},
   "source": [
    "### Basic Unpacking\n",
    "\n",
    "You can assign multiple variables from a tuple or any sequence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "e9d67087",
   "metadata": {},
   "outputs": [],
   "source": [
    "a, b = 1, 2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c0e86d9",
   "metadata": {},
   "source": [
    "Values are assigned left to right. In this example, `a` gets `1` and `b` gets `2`.\n",
    "We can display the results like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "bd0acf1f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 2)"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a, b"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3dedfbbf",
   "metadata": {},
   "source": [
    "More generally, if the left side is a tuple of variables, the right side can be any sequence - a string, list, or tuple.\n",
    "For example, to split an email address into a user name and domain:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "d3e0f3bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "email = 'monty@python.org'\n",
    "username, domain = email.split('@')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13eec68b",
   "metadata": {},
   "source": [
    "`split` returns a list with two elements; the first goes to `username`, the second to `domain`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "e6356b3b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('monty', 'python.org')"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "username, domain"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a12a2321",
   "metadata": {},
   "source": [
    "The number of variables on the left and values on the right must **match**; otherwise you get a `ValueError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "03f37c17",
   "metadata": {},
   "outputs": [
    {
     "ename": "ValueError",
     "evalue": "too many values to unpack (expected 2)",
     "output_type": "error",
     "traceback": [
      "\u001b[31mValueError\u001b[39m\u001b[31m:\u001b[39m too many values to unpack (expected 2)\n"
     ]
    }
   ],
   "source": [
    "%%expect ValueError\n",
    "\n",
    "a, b = 1, 2, 3"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ca9d55d",
   "metadata": {},
   "source": [
    "Tuple assignment is a clean way to swap two variables.\n",
    "With conventional assignments, you need a temporary variable:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "c450eadc",
   "metadata": {},
   "outputs": [],
   "source": [
    "temp = a\n",
    "a = b\n",
    "b = temp"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "60780c74",
   "metadata": {},
   "source": [
    "That works, but tuple assignment does the same swap without a temporary variable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "88c040f3",
   "metadata": {},
   "outputs": [],
   "source": [
    "a, b = b, a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "162f280a",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Tuple Assignment\n",
    "#   1. Unpack the tuple (10, 20, 30) into three variables x, y, z and print them.\n",
    "#   2. Split the email address 'ada@lovelace.org' into username and domain using\n",
    "#      split('@') and tuple assignment, then print both.\n",
    "#   3. Swap the values of two variables a=5, b=99 using tuple assignment.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "ce640574",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10 20 30\n",
      "ada lovelace.org\n",
      "99 5\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "x, y, z = (10, 20, 30)\n",
    "print(x, y, z)                                  # 10 20 30\n",
    "\n",
    "username, domain = 'ada@lovelace.org'.split('@')\n",
    "print(username, domain)                          # ada lovelace.org\n",
    "\n",
    "a, b = 5, 99\n",
    "a, b = b, a\n",
    "print(a, b)                                      # 99 5"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9dc89e7",
   "metadata": {},
   "source": [
    "### Starred Unpacking\n",
    "\n",
    "Use a starred name to capture \"the rest\" of a sequence during assignment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "78dca9c7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(0, [1, 2, 3, 4], 5)"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "first, *middle, last = range(6)\n",
    "first, middle, last"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0209e908",
   "metadata": {},
   "source": [
    "This works because all expressions on the right side are evaluated before any assignments.\n",
    "\n",
    "Tuple assignment is also handy in `for` loops.\n",
    "For example, to loop through the items in a dictionary, use the `items` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a77dfb70",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'dict_items'>\n",
      "one -> 1\n",
      "two -> 2\n"
     ]
    }
   ],
   "source": [
    "d = {'one': 1, 'two': 2}\n",
    "print(type(d.items()))\n",
    "for item in d.items():          # items() returns a dict_items view of key-value pairs\n",
    "    key, value = item\n",
    "    print(key, '->', value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "912d40e4",
   "metadata": {},
   "source": [
    "Each time through the loop, `item` is a tuple with a key and its value.\n",
    "We can unpack it directly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "0e0c0e39",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "one -> 1\n",
      "two -> 2\n"
     ]
    }
   ],
   "source": [
    "for key, value in d.items():\n",
    "    print(key, '->', value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "2da83c89",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Starred Unpacking\n",
    "#   Given data = (1, 2, 3, 4, 5, 6)\n",
    "#   1. Unpack the first element into `head`, the last into `tail`,\n",
    "#      and everything in between into `body` using starred unpacking.\n",
    "#   2. Print all three.\n",
    "#   3. Loop over {'x': 10, 'y': 20, 'z': 30}.items() and unpack\n",
    "#      each key-value pair directly in the for statement, printing each pair.\n",
    "### Your code starts here.\n",
    "\n",
    "data = (1, 2, 3, 4, 5, 6)\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "600f798c",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 [2, 3, 4, 5] 6\n",
      "x -> 10\n",
      "y -> 20\n",
      "z -> 30\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "data = (1, 2, 3, 4, 5, 6)\n",
    "\n",
    "head, *body, tail = data\n",
    "print(head, body, tail)      # 1 [2, 3, 4, 5] 6\n",
    "\n",
    "for key, value in {'x': 10, 'y': 20, 'z': 30}.items():\n",
    "    print(key, '->', value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6070e933",
   "metadata": {},
   "source": [
    "Now `key` and `value` are assigned on each iteration."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac911a12",
   "metadata": {},
   "source": [
    "## Tuple Operations\n",
    "\n",
    "Common tuple operations include: \n",
    "| Python Expression | Description | Example |\n",
    "|---|---|---|\n",
    "| `len(tuple)` | Returns the number of items in the tuple. | `len((1, 2, 3))` returns `3`. |\n",
    "| `tuple1 + tuple2` | Concatenation: combines two tuples into a new tuple. | `(1, 2) + (3, 4)` returns `(1, 2, 3, 4)`. |\n",
    "| `tuple * n` | Repetition: repeats tuple elements `n` times. | `('Hi!',) * 3` returns `('Hi!', 'Hi!', 'Hi!')`. |\n",
    "| `item in tuple` | Membership: checks whether a value exists in the tuple (`True`/`False`). | `3 in (1, 2, 3)` returns `True`. |\n",
    "| `tuple[index]` | Indexing: accesses an element using positive or negative index. | `('a', 'b', 'c')[0]` returns `'a'`. |\n",
    "| `tuple[start:stop]` | Slicing: extracts a range of elements as a new tuple. | `(1, 2, 3, 4)[1:3]` returns `(2, 3)`. |\n",
    "| `tuple(iterable)` | Constructor: converts an iterable (list, string, set, etc.) into a tuple. | `tuple([1, 2])` returns `(1, 2)`. |\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6cb8476d",
   "metadata": {},
   "source": [
    "### Tuple Operators"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9944c879",
   "metadata": {},
   "source": [
    "The `+` operator concatenates tuples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 136,
   "id": "34778c59",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('Chicago', 'Detroit', 'Minneapolis', 'Atlanta', 'Dallas', 'Miami')\n",
      "<class 'tuple'>\n"
     ]
    }
   ],
   "source": [
    "north = ('Chicago', 'Detroit', 'Minneapolis')\n",
    "south = ('Atlanta', 'Dallas', 'Miami')\n",
    "\n",
    "print(north + south)\n",
    "print(type(north + south))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21462154",
   "metadata": {},
   "source": [
    "But you cannot concatenate a string and a tuple:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 137,
   "id": "b3cf5a61",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Chicago-Detroit-Minneapolis\n",
      "<class 'str'>\n",
      "<class 'tuple'>\n"
     ]
    },
    {
     "ename": "TypeError",
     "evalue": "can only concatenate str (not \"tuple\") to str",
     "output_type": "error",
     "traceback": [
      "\u001b[31mTypeError\u001b[39m\u001b[31m:\u001b[39m can only concatenate str (not \"tuple\") to str\n"
     ]
    }
   ],
   "source": [
    "%%expect TypeError\n",
    "\n",
    "north_str = 'Chicago-Detroit-Minneapolis'   # a string, not a tuple\n",
    "print(north_str)\n",
    "print(type(north_str))          # A string is not a tuple, even if it looks like a list of items.\n",
    "\n",
    "south = ('Atlanta', 'Dallas', 'Miami')\n",
    "print(type(south))\n",
    "\n",
    "north_str + south   # TypeError: can only concatenate str to str, not tuple"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef3e843f",
   "metadata": {},
   "source": [
    "The `*` operator repeats a tuple a given number of times."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 138,
   "id": "fd858bfe",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4')"
      ]
     },
     "execution_count": 138,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "('Q1', 'Q2', 'Q3', 'Q4') * 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "92cae294",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Tuple Operators\n",
    "#   1. Concatenate (1, 2, 3) and (4, 5, 6) into a new tuple and print it.\n",
    "#   2. Repeat the tuple ('ha',) three times and print the result.\n",
    "#   3. Check whether the value 7 is in (1, 3, 5, 7, 9) and print the boolean result.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "3e7afa90",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(1, 2, 3, 4, 5, 6)\n",
      "('ha', 'ha', 'ha')\n",
      "True\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "print((1, 2, 3) + (4, 5, 6))        # concatenation\n",
    "print(('ha',) * 3)                   # repetition\n",
    "print(7 in (1, 3, 5, 7, 9))         # membership"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c941fce0",
   "metadata": {},
   "source": [
    "Based on these examples, tuples can look a lot like lists."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d1aa8aa",
   "metadata": {},
   "source": [
    "### Tuple Methods\n",
    "\n",
    "Tuples have a small set of methods. Tuples are:\n",
    "\n",
    "- Immutable (cannot be changed)\n",
    "- Fixed-size\n",
    "- Designed for safety and structure\n",
    "\n",
    "Because you can't modify them, they don’t have methods like:\n",
    "- .append()\n",
    "- .remove()\n",
    "- .sort(). \n",
    " \n",
    "The most common are `count` and `index`, which return information rather than modifying the tuple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "id": "17512c9d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(2, 3)"
      ]
     },
     "execution_count": 139,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "orders = ('PENDING', 'SHIPPED', 'PENDING', 'DELIVERED')\n",
    "orders.count('PENDING'), orders.index('DELIVERED')   # this returns a tuple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "382e406c",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Tuple Methods\n",
    "#   Given t = (3, 1, 4, 1, 5, 9, 2, 6, 5, 3)\n",
    "#   1. Count how many times 1 appears in t.\n",
    "#   2. Find the index of the first occurrence of 9.\n",
    "#   3. Count how many times 5 appears.\n",
    "### Your code starts here.\n",
    "\n",
    "t = (3, 1, 4, 1, 5, 9, 2, 6, 5, 3)\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "8bece928",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2\n",
      "5\n",
      "2\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "t = (3, 1, 4, 1, 5, 9, 2, 6, 5, 3)\n",
    "\n",
    "print(t.count(1))     # 2\n",
    "print(t.index(9))     # 5\n",
    "print(t.count(5))     # 2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31beb39a",
   "metadata": {},
   "source": [
    "### Tuple Functions\n",
    "\n",
    "In addition to these two methods, Python's built-in functions (e.g., `len`, `max`, `min`, `sum`) can be used with tuples, as they work with any `iterable`. These functions do not **modify** the original tuple, but return **new** values or objects: \n",
    "\n",
    "| Function | What it does | Notes |\n",
    "|---|---|---|\n",
    "| `len(tuple)` | Returns the total number of items in the tuple. | Works for any tuple. |\n",
    "| `max(tuple)` | Returns the largest item in the tuple. | Elements must be comparable. |\n",
    "| `min(tuple)` | Returns the smallest item in the tuple. | Elements must be comparable. |\n",
    "| `sum(tuple)` | Returns the sum of all numeric elements in the tuple. | Elements should be numbers. |\n",
    "| `sorted(tuple)` | Returns a new list with tuple elements in sorted order. | Output type is **`list`**, not `tuple`. |\n",
    "| `reversed(tuple)` | Returns an iterator over tuple elements in reverse order. | Convert with `tuple(...)` or `list(...)` if needed. |\n",
    "| `tuple(iterable)` | Converts another iterable (like a list or string) into a tuple. | Useful for type conversion. |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd7ae0d8",
   "metadata": {},
   "source": [
    "The `sorted` function works with tuples, but it returns a `list`, which is mutable for your manipulation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 150,
   "id": "82f42e9d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('RECEIVED', 'PENDING', 'SHIPPED', 'DELIVERED')\n",
      "<class 'tuple'>\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "['DELIVERED', 'PENDING', 'RECEIVED', 'SHIPPED']"
      ]
     },
     "execution_count": 150,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "orders = ('RECEIVED', 'PENDING', 'SHIPPED', 'DELIVERED')\n",
    "\n",
    "print(orders)\n",
    "print(type(orders))\n",
    "\n",
    "sorted(orders)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 151,
   "id": "1dffc1ed",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'tuple' object has no attribute 'sort'",
     "output_type": "error",
     "traceback": [
      "\u001b[31mAttributeError\u001b[39m\u001b[31m:\u001b[39m 'tuple' object has no attribute 'sort'\n"
     ]
    }
   ],
   "source": [
    "%%expect AttributeError\n",
    "\n",
    "orders.sort()  # this doesn't work because tuples are immutable"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "517af861",
   "metadata": {},
   "source": [
    "The `reversed` function also works with tuples. It returns a `reversed` object, which you can convert to a list or tuple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 146,
   "id": "373582a9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<reversed object at 0x10edf9a80>\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "('DELIVERED', 'PENDING', 'SHIPPED', 'PENDING')"
      ]
     },
     "execution_count": 146,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print(reversed(orders))\n",
    "tuple(reversed(orders))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 155,
   "id": "eff9f464",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('RECEIVED', 'PENDING', 'SHIPPED', 'DELIVERED')"
      ]
     },
     "execution_count": 155,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# original object is unchanged\n",
    "orders"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "019bbb85",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Tuple Functions\n",
    "#   Given t = (4, 7, 2, 9, 1, 5)\n",
    "#   1. Print the length, max, min, and sum of t.\n",
    "#   2. Print a sorted version of t (as a list).\n",
    "#   3. Print t in reverse order as a tuple.\n",
    "### Your code starts here.\n",
    "\n",
    "t = (4, 7, 2, 9, 1, 5)\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "123790d3",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "6 9 1 28\n",
      "[1, 2, 4, 5, 7, 9]\n",
      "(5, 1, 9, 2, 7, 4)\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "t = (4, 7, 2, 9, 1, 5)\n",
    "\n",
    "print(len(t), max(t), min(t), sum(t))  # 6 9 1 28\n",
    "print(sorted(t))                         # [1, 2, 4, 5, 7, 9]\n",
    "print(tuple(reversed(t)))                # (5, 1, 9, 2, 7, 4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "742f8b69",
   "metadata": {},
   "source": [
    "### `zip()`\n",
    "\n",
    "Tuples are useful for pairing elements from multiple sequences and working with them together.\n",
    "For example, suppose a sales team has weekly targets, and we record their actual sales alongside those targets in two separate lists."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 157,
   "id": "9cb8116e",
   "metadata": {},
   "outputs": [],
   "source": [
    "actual = [112, 98, 135, 144, 101, 129, 88]\n",
    "target = [120, 120, 120, 120, 120, 120, 120]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "462715d1",
   "metadata": {},
   "source": [
    "Let's count how many weeks the team exceeded their target.\n",
    "We'll use `zip`, a built-in function that combines sequences and returns a **zip object**, pairing elements like the teeth of a zipper."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 174,
   "id": "815cc4d3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<zip at 0x10eefc700>"
      ]
     },
     "execution_count": 174,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "zip(actual, target)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8d7649c",
   "metadata": {},
   "source": [
    "`zip` stops when the shortest input is exhausted. If you want to keep going, use `itertools.zip_longest`. The parameter **fillvalue** can be helpful filling the empty elements."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 176,
   "id": "eaf44ca6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('a', 1), ('b', 2), ('c', '-')]"
      ]
     },
     "execution_count": 176,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from itertools import zip_longest\n",
    "\n",
    "list(zip('abc', [1, 2]))\n",
    "list(zip_longest('abc', [1, 2], fillvalue='-'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df01b614",
   "metadata": {},
   "source": [
    "We can loop over the zip object to get pairwise values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 164,
   "id": "b4b7150e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(112, 120)\n",
      "(98, 120)\n",
      "(135, 120)\n",
      "(144, 120)\n",
      "(101, 120)\n",
      "(129, 120)\n",
      "(88, 120)\n"
     ]
    }
   ],
   "source": [
    "for pair in zip(actual, target):\n",
    "    print(pair)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55d41582",
   "metadata": {},
   "source": [
    "Each time through the loop, `pair` is a tuple of `(actual_sales, target_sales)`.\n",
    "We can unpack those values and count the weeks where actual sales exceeded the target:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16642aa9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "weeks_above_target = 0\n",
    "for sale, goal in zip(actual, target):\n",
    "    if sale > goal:\n",
    "        weeks_above_target += 1\n",
    "\n",
    "weeks_above_target"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2e6a7bc",
   "metadata": {},
   "source": [
    "The team beat their weekly target three out of seven weeks.\n",
    "\n",
    "If you want a list of pairs, combine `zip` with `list`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 169,
   "id": "114766fc",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[(112, 120),\n",
       " (98, 120),\n",
       " (135, 120),\n",
       " (144, 120),\n",
       " (101, 120),\n",
       " (129, 120),\n",
       " (88, 120)]"
      ]
     },
     "execution_count": 169,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "weekly_results = list(zip(actual, target))\n",
    "weekly_results"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "96db88c1",
   "metadata": {},
   "source": [
    "The result is a list of tuples, so we can get the last week's data like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 224,
   "id": "79867eb7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(88, 120)"
      ]
     },
     "execution_count": 224,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "weekly_results[-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4bd9afd4",
   "metadata": {},
   "source": [
    "If you have a list of keys and a list of values, you can use `zip` and `dict` to build a dictionary.\n",
    "Here is a mapping from each letter to its position in the alphabet."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 173,
   "id": "700e2ee1",
   "metadata": {},
   "outputs": [],
   "source": [
    "letters = 'abcdefghijklmnopqrstuvwxyz'\n",
    "numbers = range(len(letters))\n",
    "letter_map = dict(zip(letters, numbers))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d864de84",
   "metadata": {},
   "source": [
    "Now we can look up a letter and get its index in the alphabet.\n",
    "In this mapping, the index of `'a'` is `0` and the index of `'z'` is `25`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 172,
   "id": "60d074ea",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(0, 25)"
      ]
     },
     "execution_count": 172,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "letter_map['a'], letter_map['z']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bbe253a8",
   "metadata": {},
   "source": [
    "### `enumerate()`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "74e320d9",
   "metadata": {},
   "source": [
    "If you need to loop through elements and their indices, use Python the built-in function `enumerate`. The result is an enumerate object that yields pairs containing an index (starting at 0) and the corresponding element, which you then **unpack** into variables to use."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 126,
   "id": "48785c37",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0 apple\n",
      "1 banana\n",
      "2 cherry\n"
     ]
    }
   ],
   "source": [
    "fruits = ['apple', 'banana', 'cherry']\n",
    "\n",
    "for i, fruit in enumerate(fruits):\n",
    "    print(i, fruit)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "823a10af",
   "metadata": {},
   "source": [
    "If you want indices to start at 1 (like line numbers), pass the optional `start` argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 124,
   "id": "6ce7cd97",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 a\n",
      "2 b\n",
      "3 c\n"
     ]
    }
   ],
   "source": [
    "for index, element in enumerate('abc', start=1):\n",
    "    print(index, element)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "id": "7dc880c4",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Zip\n",
    "#   1. Given names = ['Alice', 'Bob', 'Carol'] and scores = [88, 95, 72],\n",
    "#      use zip to print each name paired with their score.\n",
    "#   2. Build a dictionary from these two lists using zip and dict().\n",
    "#   3. Use enumerate (starting at 1) to print each name with its rank number.\n",
    "### Your code starts here.\n",
    "\n",
    "names = ['Alice', 'Bob', 'Carol']\n",
    "scores = [88, 95, 72]\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "id": "5bda66b3",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Alice 88\n",
      "Bob 95\n",
      "Carol 72\n",
      "{'Alice': 88, 'Bob': 95, 'Carol': 72}\n",
      "1 Alice\n",
      "2 Bob\n",
      "3 Carol\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "names = ['Alice', 'Bob', 'Carol']\n",
    "scores = [88, 95, 72]\n",
    "\n",
    "for name, score in zip(names, scores):\n",
    "    print(name, score)\n",
    "\n",
    "scoreboard = dict(zip(names, scores))\n",
    "print(scoreboard)\n",
    "\n",
    "for rank, name in enumerate(names, start=1):\n",
    "    print(rank, name)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be14b0aa",
   "metadata": {},
   "source": [
    "### Comparing and Sorting\n",
    "\n",
    "Relational operators work with tuples and other sequences.\n",
    "Tuple comparison is lexicographic: it compares the first elements, then the next, and so on until it finds a difference.\n",
    "For example, we can represent a reporting period as a `(year, quarter)` tuple and compare two periods."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 177,
   "id": "6f143fb9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 177,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "(2026, 1) < (2026, 3)    # Q1 2026 is before Q3 2026"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90d04fc6",
   "metadata": {},
   "source": [
    "Once a difference is found, later elements are not considered."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 178,
   "id": "38d79bd4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 178,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "(2026, 1, 999_999) < (2026, 3, 62_000)   # Q1 beats Q3 in revenue, but Q3 comes later"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ae08e64",
   "metadata": {},
   "source": [
    "This comparison behavior is useful for sorting lists of tuples or finding minimum and maximum values.\n",
    "As an example, let's find the most common letter in a word — a step often used in text analytics.\n",
    "In the previous chapter, we wrote `value_counts`, which returns a dictionary mapping each letter to its count."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 190,
   "id": "64d8a296",
   "metadata": {},
   "outputs": [],
   "source": [
    "def value_counts(string):\n",
    "    counter = {}\n",
    "    for letter in string:\n",
    "        if letter not in counter:\n",
    "            counter[letter] = 1\n",
    "        else:\n",
    "            counter[letter] += 1\n",
    "    return counter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34c6e9f1",
   "metadata": {},
   "source": [
    "Here is the result for the string `'mississippi'`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 191,
   "id": "f3d047cf",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'m': 1, 'i': 4, 's': 4, 'p': 2}"
      ]
     },
     "execution_count": 191,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "counter = value_counts('mississippi')\n",
    "counter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b74cddce",
   "metadata": {},
   "source": [
    "With eight distinct letters, it's not immediately obvious which one appears most often.\n",
    "Sorting the items makes the answer clear.\n",
    "\n",
    "We can get the items from `counter` like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 192,
   "id": "b38e0a7f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict_items([('m', 1), ('i', 4), ('s', 4), ('p', 2)])"
      ]
     },
     "execution_count": 192,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "items = counter.items()\n",
    "items"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d388df19",
   "metadata": {},
   "source": [
    "The result is a `dict_items` object that behaves like a list of tuples, so we can sort it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 193,
   "id": "587d88e5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('i', 4), ('m', 1), ('p', 2), ('s', 4)]"
      ]
     },
     "execution_count": 193,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sorted(items)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "id": "33d08ef3",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Comparing and Sorting\n",
    "#   1. Given a list of (last_name, first_name) tuples below,\n",
    "#      sort it lexicographically (default tuple sort) and print the result.\n",
    "#   2. What does Python compare first — last name or first name?\n",
    "people = [('Smith', 'John'), ('Adams', 'Zara'), ('Smith', 'Alice'), ('Adams', 'Alan')]\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "id": "5c7253c7",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[('Adams', 'Alan'), ('Adams', 'Zara'), ('Smith', 'Alice'), ('Smith', 'John')]\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "people = [('Smith', 'John'), ('Adams', 'Zara'), ('Smith', 'Alice'), ('Adams', 'Alan')]\n",
    "print(sorted(people))\n",
    "# Python compares the first element (last name) first;\n",
    "# if equal, it compares the second element (first name).\n",
    "# Result: [('Adams', 'Alan'), ('Adams', 'Zara'), ('Smith', 'Alice'), ('Smith', 'John')]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d6a333e",
   "metadata": {},
   "source": [
    "### Sorting by Value\n",
    "\n",
    "Sometimes you want to sort dictionary items by their values rather than their keys.\n",
    "We can define a small helper that returns the second element of a `(key, value)` pair."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b533288e",
   "metadata": {},
   "source": [
    "(second_element)="
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "id": "c75fa385",
   "metadata": {},
   "outputs": [],
   "source": [
    "def second_element(t):\n",
    "    return t[1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ee375ee",
   "metadata": {},
   "source": [
    "Then we pass that function as the optional `key` argument to `sorted`.\n",
    "The `key` function computes a **sort key** for each item."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "id": "ae127f5d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('b', 1), ('n', 2), ('a', 3)]"
      ]
     },
     "execution_count": 77,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sorted_items = sorted(items, key=second_element)\n",
    "sorted_items"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a250cd2",
   "metadata": {},
   "source": [
    "The sort key determines the order.\n",
    "The letter with the lowest count appears first, and the highest count appears last.\n",
    "So we can find the most common letter like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "id": "ca98f67c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('a', 3)"
      ]
     },
     "execution_count": 78,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sorted_items[-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "896af134",
   "metadata": {},
   "source": [
    "If we only want the maximum, we don't have to sort the list.\n",
    "We can use `max`, which also accepts a `key` function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "id": "6f8dc73d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('a', 3)"
      ]
     },
     "execution_count": 79,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "max(items, key=second_element)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 80,
   "id": "c1be6eb0",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Sorting by Value\n",
    "#   1. Count the letter frequencies in the word 'mississippi'.\n",
    "#   2. Sort the resulting items by frequency (value), ascending.\n",
    "#   3. Print the letter with the highest frequency using max() with a key function.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 81,
   "id": "2d68c835",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[('m', 1), ('p', 2), ('i', 4), ('s', 4)]\n",
      "('i', 4)\n"
     ]
    }
   ],
   "source": [
    "### solution\n",
    "\n",
    "counter = value_counts('mississippi')\n",
    "items = counter.items()\n",
    "\n",
    "def second_element(t):\n",
    "    return t[1]\n",
    "\n",
    "print(sorted(items, key=second_element))   # sorted by frequency ascending\n",
    "print(max(items, key=second_element))      # ('i', 4) — most frequent letter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28ce6533",
   "metadata": {},
   "source": [
    "To find the letter with the lowest count, we could use `min` the same way."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67fd7e0c",
   "metadata": {},
   "source": [
    "## Tuples and Functions"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e647515",
   "metadata": {},
   "source": [
    "### Tuples as Return Values\n",
    "\n",
    "**A function returns one object**, but that object can be a tuple.\n",
    "Returning a tuple is a common way to provide **multiple** results.\n",
    "\n",
    "For example, a scheduling system may need to convert a total number of minutes into hours and minutes.\n",
    "The built-in function `divmod` (`//` and `%`) takes two arguments and returns a tuple with the quotient and remainder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 194,
   "id": "64b83062",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(2, 15)"
      ]
     },
     "execution_count": 194,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "divmod(135, 60)    # 135 minutes → (2 hours, 15 minutes)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b5ce6cc",
   "metadata": {},
   "source": [
    "We can use tuple assignment to store the hours and minutes in separate variables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 195,
   "id": "208be1eb",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2"
      ]
     },
     "execution_count": 195,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hours, minutes = divmod(135, 60)\n",
    "hours"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "056b3902",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 84,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "minutes"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b183272e",
   "metadata": {},
   "source": [
    "Here is a simple function that returns both the minimum and maximum from a list — useful for summarizing a range of values, such as daily sales figures."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "id": "a93d62d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def min_max(t):\n",
    "    return min(t), max(t)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "016fd796",
   "metadata": {},
   "source": [
    "`min` and `max` are built-in functions that find the smallest and largest elements of a sequence.\n",
    "`min_max` computes both and returns them as a tuple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 201,
   "id": "9c73a695",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(7200, 12400)"
      ]
     },
     "execution_count": 201,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "min_max([8500, 12400, 7200, 9900])    # daily sales figures"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9cd8b4e",
   "metadata": {},
   "source": [
    "We can unpack the result like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c00f8d05",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 4)"
      ]
     },
     "execution_count": 87,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "low, high = min_max([8500, 12400, 7200, 9900])\n",
    "low, high"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 88,
   "id": "31aa277d",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Tuples as Return Values\n",
    "#   Write a function called `stats` that takes a list of numbers and returns\n",
    "#   a tuple containing (min, max, sum, mean) of the list.\n",
    "#   Test it with [10, 20, 30, 40, 50] and unpack the result into four variables.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 89,
   "id": "7f31585a",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10 50 150 30.0\n"
     ]
    }
   ],
   "source": [
    "def stats(numbers):\n",
    "    return min(numbers), max(numbers), sum(numbers), sum(numbers) / len(numbers)\n",
    "\n",
    "lo, hi, total, avg = stats([10, 20, 30, 40, 50])\n",
    "print(lo, hi, total, avg)    # 10 50 150 30.0"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "63cb9f0d",
   "metadata": {},
   "source": [
    "### Argument Packing\n",
    "\n",
    "Functions can take a variable number of arguments.\n",
    "A parameter name that begins with `*` **packs** extra arguments into a tuple.\n",
    "The following function (pretending we don't have a built-in for it) takes any number of arguments and computes their arithmetic mean."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 196,
   "id": "7a2beb89",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mean(*args):                # variable-length argument list\n",
    "    return sum(args) / len(args)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "514388b0",
   "metadata": {},
   "source": [
    "The parameter name can be anything, but `args` is conventional.\n",
    "Here we call it with three employee performance scores:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 214,
   "id": "047c3f54",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9500.0"
      ]
     },
     "execution_count": 214,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mean(8500, 12400, 7200, 9900)    # average of four daily sales figures"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc83e72d",
   "metadata": {},
   "source": [
    "If you have a sequence of values and want to pass them as separate arguments, use `*` to **unpack** the sequence.\n",
    "For example, `divmod` takes exactly two arguments - if you pass a tuple, it counts as a single argument and raises an error."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 215,
   "id": "4f91f0f5",
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "divmod expected 2 arguments, got 1",
     "output_type": "error",
     "traceback": [
      "\u001b[31mTypeError\u001b[39m\u001b[31m:\u001b[39m divmod expected 2 arguments, got 1\n"
     ]
    }
   ],
   "source": [
    "%%expect TypeError\n",
    "t = (135, 60)\n",
    "divmod(t)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17ad85eb",
   "metadata": {},
   "source": [
    "Even though the tuple contains two elements, it is still one argument.\n",
    "If you unpack it, the two elements are passed separately."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 216,
   "id": "9390dce8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(2, 15)"
      ]
     },
     "execution_count": 216,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "divmod(*t)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18a060b9",
   "metadata": {},
   "source": [
    "Packing and unpacking are handy when you want to adapt an existing function.\n",
    "For example, this function takes any number of arguments, removes the lowest and highest, and computes the mean of the rest."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 217,
   "id": "ae6c283a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def trimmed_mean(*args):\n",
    "    low, high = min_max(args)\n",
    "    trimmed = list(args)        # convert to a list for mutability\n",
    "    trimmed.remove(low)\n",
    "    trimmed.remove(high)\n",
    "    return mean(*trimmed)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb767fee",
   "metadata": {},
   "source": [
    "First it uses `min_max` to find the lowest and highest values.\n",
    "Then it converts `args` to a list so it can use `remove`.\n",
    "Finally it **unpacks** the trimmed list so the elements are passed to `mean` as separate arguments.\n",
    "\n",
    "Here is an example using judge scores, where one score is unusually low:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 218,
   "id": "55f07796",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "7.75"
      ]
     },
     "execution_count": 218,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mean(8.5, 9.2, 8.8, 4.5)    # raw mean including the outlier"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 219,
   "id": "a655f1ea",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "8.65"
      ]
     },
     "execution_count": 219,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "trimmed_mean(8.5, 9.2, 8.8, 4.5)    # mean after dropping lowest (4.5) and highest (9.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b8d915ad",
   "metadata": {},
   "source": [
    "You can also unpack sequences to build new tuples (or lists) without using `+`.\n",
    "For example, merging two quarterly sales tuples into a single half-year tuple:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 220,
   "id": "4a12c0ed",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(112, 98, 135, 144, 101, 129)"
      ]
     },
     "execution_count": 220,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "q1 = (112, 98, 135)    # monthly sales for Q1\n",
    "q2 = (144, 101, 129)   # monthly sales for Q2\n",
    "h1 = (*q1, *q2)        # combined first-half data\n",
    "h1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32edf06a",
   "metadata": {},
   "source": [
    "This kind of trimmed mean is used in sports with subjective judging (like diving and gymnastics) to reduce the influence of outlier scores."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 221,
   "id": "86500fb9",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: When to Use Tuples\n",
    "#   For each scenario below, decide whether a tuple or a list is more appropriate\n",
    "#   and implement it:\n",
    "#   1. Store the (latitude, longitude) coordinates of a fixed location: 38.9, -77.0\n",
    "#   2. Build a collection of city names that will have cities added to it over time.\n",
    "#      Start with ['Paris', 'Tokyo'] and append 'Nairobi'.\n",
    "#   3. Use a (month, day) tuple as a key in a dictionary to store holiday names.\n",
    "#      Add entries for (1,1)->'New Year' and (7,4)->'Independence Day'.\n",
    "#      Print the holiday for July 4th.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 223,
   "id": "a8f708f3",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(38.9, -77.0)\n",
      "['Paris', 'Tokyo', 'Nairobi']\n",
      "Independence Day\n"
     ]
    }
   ],
   "source": [
    "# 1. Tuple — fixed record, won't change\n",
    "location = (38.9, -77.0)\n",
    "print(location)\n",
    "\n",
    "# 2. List — will grow over time\n",
    "cities = ['Paris', 'Tokyo']\n",
    "cities.append('Nairobi')\n",
    "print(cities)\n",
    "\n",
    "# 3. Tuple as dict key — tuples are hashable\n",
    "holidays = {(1, 1): 'New Year', (7, 4): 'Independence Day'}\n",
    "print(holidays[(7, 4)])    # Independence Day"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa001a8c",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": ".venv",
   "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.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
