{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f39d9d77-c336-42ad-a1a3-a4c28be72888",
   "metadata": {},
   "source": [
    "# Exceptions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "5b0b7545",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "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",
    "from shared import thinkpython, diagram, jupyturtle\n",
    "from shared.download import download\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"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2280f4f",
   "metadata": {},
   "source": [
    "**Learning goals:** By the end of this chapter you will be able to:\n",
    "\n",
    "- Distinguish between syntax errors, runtime errors, and semantic errors\n",
    "- Read and interpret Python tracebacks to locate the source of a bug\n",
    "- Handle exceptions gracefully with `try`, `except`, `else`, and `finally`\n",
    "- Catch specific exception types and provide a meaningful response to each\n",
    "- Raise exceptions with `raise` and define custom exception classes\n",
    "- Apply print-statement debugging and `assert` to isolate bugs systematically\n",
    "- Use the `logging` module to record diagnostic messages at appropriate severity levels"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce5bc465",
   "metadata": {},
   "source": [
    "## Exception Handling\n",
    "\n",
    "Exception handling is **runtime defense**. It's code that runs in production to handle conditions you expect might go wrong (`try/except`, `raise`). It's about graceful failure when something bad actually happens. Think about exception handling this way: Exception handling answers this question: \"what should my code do when things go wrong?\"\n",
    "\n",
    "When writing programs, errors are inevitable. Python distinguishes between \n",
    "- **syntax errors** (code that violates Python grammar rules) and \n",
    "- **exceptions** (errors detected during execution). \n",
    "Exception handling allows your program to respond gracefully to runtime errors instead of crashing.\n",
    "\n",
    "**Common Exception Types**\n",
    "\n",
    "Python has many built-in exception types. Here are the most common:\n",
    "\n",
    "| Exception | Description | Example |\n",
    "|-----------|-------------|---------|\n",
    "| `ValueError` | Invalid value for operation | `int(\"hello\")` |\n",
    "| `TypeError` | Wrong type for operation | `\"5\" + 5` |\n",
    "| `ZeroDivisionError` | Division by zero | `10 / 0` |\n",
    "| `IndexError` | Index out of range | `[1, 2][5]` |\n",
    "| `KeyError` | Dictionary key not found | `{}[\"missing\"]` |\n",
    "| `FileNotFoundError` | File doesn't exist | `open(\"nonexistent.txt\")` |\n",
    "| `AttributeError` | Attribute doesn't exist | `\"text\".nonexistent()` |\n",
    "| `NameError` | Variable not defined | `print(undefined_var)` |\n",
    "\n",
    "\n",
    "When executing a program and exceptions happen, without **exception handling**:\n",
    "- Programs **crash** with cryptic error messages\n",
    "- **Users** have poor experience\n",
    "- Difficult to **debug** production issues\n",
    "\n",
    "With exception handling, we can:\n",
    "- Programs can recover from errors\n",
    "- Provide user-friendly error messages\n",
    "- Log errors for debugging\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdc1d189",
   "metadata": {},
   "source": [
    "### `try`-`except`\n",
    "\n",
    "The `try`-`except` statement lets you catch and respond to exceptions instead of letting them crash your program. You wrap the risky code in a `try` block; if an exception is raised, Python jumps immediately to the matching `except` block and executes it instead.\n",
    "\n",
    "**Syntax:**\n",
    "The syntax of `try`-`except` is:\n",
    "```python\n",
    "try:\n",
    "    # Code that might raise an exception\n",
    "    risky_code()\n",
    "except ExceptionType:\n",
    "    # Handle the exception\n",
    "    handle_error()\n",
    "```\n",
    "\n",
    "A complete `try` statement can include up to four clauses, each serving a distinct role:\n",
    "\n",
    "- **`try`**: Block containing code that might raise an exception\n",
    "- **`except`**: Block to handle specific exception type(s)\n",
    "- **`else`** (optional): Block that executes if no exception occurs\n",
    "- **`finally`** (optional): Block that always executes, regardless of exception"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c5f133d",
   "metadata": {},
   "source": [
    "### `except Exception as e`\n",
    "\n",
    "Adding `as e` to an `except` clause binds the exception object to the variable `e`, so you can inspect it:\n",
    "\n",
    "```python\n",
    "except ValueError as e:\n",
    "    print(e)           # prints the error message\n",
    "    print(type(e))     # <class 'ValueError'>\n",
    "```\n",
    "\n",
    "`e` is just a conventional name; you could use any variable name (`as err`, `as exc`, etc.). Inside an f-string, `{e}` calls `str(e)` which produces the human-readable error message:\n",
    "\n",
    "```python\n",
    "except FileNotFoundError as e:\n",
    "    print(f\"Error: {e}\")   # e.g. \"Error: [Errno 2] No such file or directory: 'x.txt'\"\n",
    "```\n",
    "\n",
    "You can omit `as e` entirely when you don't need the details:\n",
    "\n",
    "```python\n",
    "except ZeroDivisionError:\n",
    "    print(\"Cannot divide by zero\")\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "b7779127",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: Cannot divide by zero!\n"
     ]
    }
   ],
   "source": [
    "%%expect ZeroDivisionError\n",
    "# Example 1: Simple try-except\n",
    "\n",
    "try:\n",
    "    result = 10 / 0\n",
    "except ZeroDivisionError:\n",
    "    print(\"Error: Cannot divide by zero!\") ### program will not crash, error is handled\n",
    "else:\n",
    "    print(f\"Result: {result}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9094abec",
   "metadata": {},
   "source": [
    "In this example, dividing by zero raises a `ZeroDivisionError`. The `except` clause catches it and prints a friendly message, so the program continues instead of crashing. Because an exception was raised, the `else` clause is skipped."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "9f196214",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: File 'non_existent_file.txt' not found!\n",
      "File operation finished.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Example 2: Practical example - safe file handling\n",
    "def read_file_safely(filename):\n",
    "    content = None\n",
    "    try:\n",
    "        with open(filename, \"r\") as file:\n",
    "            content = file.read()\n",
    "            print(f\"File '{filename}' content:\\n{content}\")\n",
    "    except FileNotFoundError:    ### may occur if the file does not exist\n",
    "        print(f\"Error: File '{filename}' not found!\")\n",
    "    except IOError as e:         ### may occur if there is an I/O error while reading the file\n",
    "        print(f\"Error reading file: {e}\")\n",
    "    else:\n",
    "        print(\"File reading completed successfully.\")\n",
    "    finally:\n",
    "        print(\"File operation finished.\\n\")\n",
    "    return content\n",
    "\n",
    "read_file_safely(\"non_existent_file.txt\")  # This will trigger the FileNotFoundError\n",
    "# read_file_safely(\"../../data/words.txt\")  # This should read the current file successfully\n",
    "# read_file_safely(__file__)  # This should read the current file successfully"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b46e3ac",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "42\n",
      "Cannot convert '3.14' to int\n",
      "None\n",
      "Cannot convert 'abc' to int\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "### Exercise: Catching Exceptions\n",
    "#   Write `safe_int(s)` that:\n",
    "#   1. Tries to convert the string `s` to an integer with `int(s)`.\n",
    "#   2. Returns the integer if conversion succeeds.\n",
    "#   3. If `s` cannot be converted, catches the exception, prints a descriptive\n",
    "#      message, and returns None.\n",
    "#   Test:\n",
    "#     safe_int('42')   → 42\n",
    "#     safe_int('3.14') → None  (prints a message)\n",
    "#     safe_int('abc')  → None  (prints a message)\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "599ebb47-1cc8-4914-83ec-6f6aa1f0e3bf",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "42\n",
      "Cannot convert '3.14' to int\n",
      "None\n",
      "Cannot convert 'abc' to int\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "\n",
    "def safe_int(s):\n",
    "    try:\n",
    "        return int(s)\n",
    "    except ValueError:\n",
    "        print(f\"Cannot convert {s!r} to int\")\n",
    "        return None\n",
    "\n",
    "print(safe_int('42'))    # 42\n",
    "print(safe_int('3.14'))  # message, then None\n",
    "print(safe_int('abc'))   # message, then None\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7dcaaf7",
   "metadata": {},
   "source": [
    "## Debugging"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51315049",
   "metadata": {},
   "source": [
    "Debugging can be frustrating, but it is also challenging, interesting, and sometimes even fun. It is one of the most important skills you can learn.\n",
    "\n",
    "**Debugging is like detective work**: You are given clues, and you have to infer the events that led to the results you see.\n",
    "\n",
    "**Debugging is like experimental science**: Once you have an idea about what is going wrong, you modify your program and try again. If your hypothesis was correct, you can predict the result of the modification. If your hypothesis was wrong, you have to come up with a new one.\n",
    "\n",
    "**Key debugging principles:**\n",
    "- Start with a working program and make small modifications\n",
    "- Test frequently; don't write too much code before testing\n",
    "- Use print statements to trace execution\n",
    "- Read error messages carefully\n",
    "- Think systematically about what could be wrong\n",
    "\n",
    "If you find yourself spending a lot of time debugging, that's often a sign you're writing too much code before testing. Taking smaller steps can help you move more quickly."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be400353",
   "metadata": {},
   "source": [
    "### Understanding Errors\n",
    "\n",
    "When you write programs, things will go wrong. Learning to find and fix these problems is called **debugging**. Before you can handle errors gracefully, you need to understand what types of errors exist and how to find them.\n",
    "\n",
    "**Types of Errors:**\n",
    "\n",
    "| Error Type | When It Occurs | Example |\n",
    "|------------|----------------|---------|\n",
    "| **Syntax Error** | Code violates Python grammar | Missing colon, unmatched quotes |\n",
    "| **Runtime Error (Exceptions)** | Error during execution | Division by zero, file not found |\n",
    "| **Logic (Semantic) Error** | Code runs but produces wrong results | Incorrect algorithm, wrong formula |\n",
    "\n",
    "When Python encounters a **runtime error**, it raises an **exception**. Without proper handling, **exceptions cause programs to crash with error messages** called **tracebacks**. "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "285a643b",
   "metadata": {},
   "source": [
    "### Reading Tracebacks\n",
    "\n",
    "When Python encounters a runtime error, it displays a **traceback** - an error report that shows where the error occurred. Learning to read tracebacks is essential for debugging.\n",
    "\n",
    "The error message includes a **traceback**, which shows:\n",
    "1. **Where** the error occurred  \n",
    "2. Which **lines** were executed leading to the error\n",
    "3. What **type of error** happened\n",
    "\n",
    "The order of functions in the traceback matches the order of function calls: you read it from **bottom to top**.\n",
    "\n",
    "- **Bottom**: The error type and message (e.g., \"NameError: name 'cat' is not defined\")\n",
    "- **Middle**: The line where the error actually happened  \n",
    "- **Top**: The chain of function calls that led there"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "a1635bcf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example: Demonstrating traceback reading and debugging\n",
    "\n",
    "def print_twice(value):\n",
    "    \"\"\"Print a value twice - contains a deliberate bug for demonstration.\"\"\"\n",
    "    print(value)\n",
    "    print(cat)          # Bug: 'cat' is not defined\n",
    "\n",
    "def cat_twice():\n",
    "    \"\"\"Function that calls print_twice.\"\"\"\n",
    "    line1 = \"Bing tiddle \"\n",
    "    line2 = \"tiddle bang.\"\n",
    "    print_twice(line1)\n",
    "    print_twice(line2)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b649c3a3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The above would produce a traceback like this:\n",
      "\n",
      "Traceback (most recent call last):\n",
      "  File \"debug_example.py\", line 15, in <module>\n",
      "    cat_twice()\n",
      "  File \"debug_example.py\", line 11, in cat_twice\n",
      "    print_twice(line1)\n",
      "  File \"debug_example.py\", line 4, in print_twice\n",
      "    print(cat)\n",
      "NameError: name 'cat' is not defined\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Uncomment the line below to see the traceback\n",
    "# cat_twice()\n",
    "\n",
    "print(\"The above would produce a traceback like this:\")\n",
    "print(\"\"\"\n",
    "Traceback (most recent call last):\n",
    "  File \"debug_example.py\", line 15, in <module>\n",
    "    cat_twice()\n",
    "  File \"debug_example.py\", line 11, in cat_twice\n",
    "    print_twice(line1)\n",
    "  File \"debug_example.py\", line 4, in print_twice\n",
    "    print(cat)\n",
    "NameError: name 'cat' is not defined\n",
    "\"\"\")\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "877b0ce1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Debugging steps:\n",
      "1. Read from bottom up: NameError on line 4 in print_twice()\n",
      "2. The undefined variable is 'cat'\n",
      "3. Trace back: cat_twice() called print_twice() with line1\n",
      "4. Fix: Change 'cat' to 'value' in print_twice()\n",
      "\n",
      "==================================================\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Debugging steps:\n",
    "print(\"Debugging steps:\")\n",
    "print(\"1. Read from bottom up: NameError on line 4 in print_twice()\")\n",
    "print(\"2. The undefined variable is 'cat'\") \n",
    "print(\"3. Trace back: cat_twice() called print_twice() with line1\")\n",
    "print(\"4. Fix: Change 'cat' to 'value' in print_twice()\")\n",
    "\n",
    "print(\"\\n\" + \"=\"*50 + \"\\n\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19e0ea56",
   "metadata": {},
   "source": [
    "### Debugging Techniques\n",
    "\n",
    "Three practical techniques for tracking down bugs:\n",
    "\n",
    "**1. Print-statement debugging** — Insert temporary `print()` calls to inspect variable values and confirm which lines of code are actually running. Prefix them with `DEBUG:` so they are easy to find and remove later. This is the simplest technique and works well for most everyday bugs.\n",
    "\n",
    "**2. Assertions** — Use `assert` to document assumptions your code makes (e.g., \"this argument must be a non-negative integer\"). If the assumption is ever violated, Python raises an `AssertionError` immediately at the offending line rather than silently producing a wrong result later. Assertions are for *developer* checks — they can be disabled globally with `python -O`, so never use them for input validation in production code.\n",
    "\n",
    "The syntax of `assert` is:\n",
    "```python\n",
    "assert condition\n",
    "assert condition, \"message shown if condition is False\"\n",
    "```\n",
    "For example,\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0db2d20",
   "metadata": {},
   "outputs": [
    {
     "ename": "AssertionError",
     "evalue": "Expected int, got float",
     "output_type": "error",
     "traceback": [
      "\u001b[31mAssertionError\u001b[39m\u001b[31m:\u001b[39m Expected int, got float\n"
     ]
    }
   ],
   "source": [
    "%%expect AssertionError\n",
    "\n",
    "items = [\"Alarm Clock\", \"Backpack\", \"Candle\", \"Doll\"]\n",
    "n = 42.0\n",
    "\n",
    "assert 2 + 2 == 4               # passes silently\n",
    "assert len(items) > 0           # passes if list is non-empty\n",
    "\n",
    "assert isinstance(n, int), f\"Expected int, got {type(n).__name__}\" ### fails w/ message"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a6d4f02",
   "metadata": {},
   "source": [
    ":::{dropdown} What is `__name__`?\n",
    "Every Python type has a built-in attribute `__name__` that holds the type's readable name as a plain string of  is a special built-in attribute that exists on classes, functions, and modules:\n",
    "\n",
    "    type(42).__name__       # 'int'\n",
    "    type(\"hello\").__name__  # 'str'\n",
    "    type(3.14).__name__     # 'float'\n",
    "    type(None).__name__     # 'NoneType'\n",
    "\n",
    "In exception handling you often use it to produce a readable error message without hard-coding a type name:\n",
    "\n",
    "    if not isinstance(age, (int, float)):\n",
    "        raise TypeError(f\"Age must be a number, got {type(age).__name__}\")\n",
    "\n",
    "If someone passes `\"twenty\"`, the message reads `\"Age must be a number, got str\"` rather than `\"Age must be a number, got <class 'str'>\"`.\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e5e17073",
   "metadata": {},
   "source": [
    "For example, if you run Python with the `-O` flag: `python -O myscript.py`, every assert in your entire program is silently skipped and the checks never run. In summary:\n",
    "\n",
    "- assert = \"this should never be False if my code is correct\" (catching your own bugs during development)\n",
    "- raise = \"this input is invalid and I need to handle it\" (defending against bad data at runtime)\n",
    "\n",
    "\n",
    "**3. Systematic bisection** — When a bug is hard to pin down, divide and conquer: comment out half the suspect code (or add a print halfway through) and confirm whether the bug still appears. Keep halving until you isolate the exact line. This beats reading every line top-to-bottom."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "560693fd",
   "metadata": {},
   "source": [
    "**Print-statement debugging**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "442b4dc5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Print Statement Debugging ===\n",
      "DEBUG: Input numbers = [10, 20, 30, 40]\n",
      "DEBUG: Length = 4\n",
      "DEBUG: Step 1: added 10, total now = 10\n",
      "DEBUG: Step 2: added 20, total now = 30\n",
      "DEBUG: Step 3: added 30, total now = 60\n",
      "DEBUG: Step 4: added 40, total now = 100\n",
      "DEBUG: Final average = 25.0\n",
      "Result: 25.0\n",
      "\n",
      "==================================================\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def debug_with_print_statements():\n",
    "    \"\"\"Demonstrate print statement debugging.\"\"\"\n",
    "    print(\"=== Print Statement Debugging ===\")\n",
    "    \n",
    "    def calculate_average(numbers):\n",
    "        print(f\"DEBUG: Input numbers = {numbers}\")\n",
    "        print(f\"DEBUG: Length = {len(numbers)}\")\n",
    "        \n",
    "        total = 0\n",
    "        for i, num in enumerate(numbers):\n",
    "            total += num\n",
    "            print(f\"DEBUG: Step {i+1}: added {num}, total now = {total}\")\n",
    "        \n",
    "        average = total / len(numbers)\n",
    "        print(f\"DEBUG: Final average = {average}\")\n",
    "        return average\n",
    "    \n",
    "    # Test with example data\n",
    "    test_numbers = [10, 20, 30, 40]\n",
    "    result = calculate_average(test_numbers)\n",
    "    print(f\"Result: {result}\")\n",
    "\n",
    "debug_with_print_statements()\n",
    "\n",
    "print(\"\\n\" + \"=\"*50 + \"\\n\")\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b602a2c",
   "metadata": {},
   "source": [
    "**Assertions**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "6fd8d26d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Assertion Debugging ===\n",
      "Computing factorial of 5\n",
      "factorial(5) = 120\n",
      "Computing factorial of 0\n",
      "factorial(0) = 1\n",
      "Computing factorial of 1\n",
      "factorial(1) = 1\n"
     ]
    }
   ],
   "source": [
    "def debug_with_assertions():\n",
    "    \"\"\"Demonstrate assertion debugging.\"\"\"\n",
    "    print(\"=== Assertion Debugging ===\")\n",
    "    \n",
    "    def factorial(n):\n",
    "        # Assertions help catch problems early\n",
    "        assert isinstance(n, int), f\"Expected int, got {type(n)}\"\n",
    "        assert n >= 0, f\"Expected non-negative number, got {n}\"\n",
    "        \n",
    "        print(f\"Computing factorial of {n}\")\n",
    "        \n",
    "        if n == 0 or n == 1:\n",
    "            return 1\n",
    "        \n",
    "        result = 1\n",
    "        for i in range(2, n + 1):\n",
    "            result *= i\n",
    "            # Assertion to check intermediate results\n",
    "            assert result > 0, f\"Unexpected negative result at step {i}\"\n",
    "        \n",
    "        return result\n",
    "    \n",
    "    # Test cases\n",
    "    test_cases = [5, 0, 1]\n",
    "    for test in test_cases:\n",
    "        try:\n",
    "            result = factorial(test)\n",
    "            print(f\"factorial({test}) = {result}\")\n",
    "        except AssertionError as e:\n",
    "            print(f\"Assertion failed for factorial({test}): {e}\")\n",
    "\n",
    "debug_with_assertions()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a55d06eb",
   "metadata": {},
   "source": [
    "**Systematic bisection**\n",
    "\n",
    "Bisection debugging means *divide and conquer*: when a multi-step function produces the wrong answer and you don't know which step is at fault, add a checkpoint halfway through and check whether the intermediate data is already wrong at that point. If it is, the bug is in the first half; if not, it's in the second half. Keep halving until you isolate the exact line.\n",
    "\n",
    "The example below has a three-stage pipeline with a real bug. The bisection steps show exactly how to narrow it down without reading every line."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "a0a46a01",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Result:   10.4\n",
      "Expected: 13.0\n",
      "\n",
      "DEBUG midpoint — celsius  = [0.0, 100.0, 37.0, 25.0, -10.0]\n",
      "DEBUG midpoint — filtered = [0.0, 37.0, 25.0, -10.0]\n",
      "\n",
      "DEBUG Stage 3 — sum = 52.0, dividing by len(celsius) = 5\n",
      "Bug found: 'len(celsius)' should be 'len(filtered)'\n",
      "\n",
      "Fixed result: 13.0\n"
     ]
    }
   ],
   "source": [
    "def analyze_temperatures(readings):\n",
    "    \"\"\"\n",
    "    Convert Fahrenheit readings to Celsius, discard outliers\n",
    "    (below -10 °C or above 45 °C), then return the average.\n",
    "\n",
    "    >>> analyze_temperatures([32, 212, 98.6, 77, 14])\n",
    "    13.0\n",
    "    \"\"\"\n",
    "    # Stage 1: convert to Celsius\n",
    "    celsius = [(f - 32) * 5 / 9 for f in readings]\n",
    "\n",
    "    # Stage 2: filter outliers\n",
    "    filtered = [t for t in celsius if -10 <= t <= 45]\n",
    "\n",
    "    # Stage 3: compute average  ← bug is here\n",
    "    average = sum(filtered) / len(celsius)   # wrong: divides by original length\n",
    "    return average\n",
    "\n",
    "\n",
    "readings = [32, 212, 98.6, 77, 14]\n",
    "print(f\"Result:   {analyze_temperatures(readings):.1f}\")  # prints 10.4\n",
    "print(f\"Expected: 13.0\")\n",
    "\n",
    "# --- Bisection step 1: checkpoint after Stage 2 (the midpoint of three stages) ---\n",
    "celsius  = [(f - 32) * 5 / 9 for f in readings]\n",
    "filtered = [t for t in celsius if -10 <= t <= 45]\n",
    "print(f\"\\nDEBUG midpoint — celsius  = {[round(t, 1) for t in celsius]}\")\n",
    "print(f\"DEBUG midpoint — filtered = {[round(t, 1) for t in filtered]}\")\n",
    "# celsius  = [0.0, 100.0, 37.0, 25.0, -10.0]\n",
    "# filtered = [0.0, 37.0, 25.0, -10.0]   ← correct (100.0 filtered out, 4 values remain)\n",
    "# Stages 1 and 2 are fine; the bug must be in Stage 3.\n",
    "\n",
    "# --- Bisection step 2: inspect Stage 3 ---\n",
    "print(f\"\\nDEBUG Stage 3 — sum = {sum(filtered):.1f}, dividing by len(celsius) = {len(celsius)}\")\n",
    "# sum = 52.0, dividing by 5  ← wrong! should be len(filtered) = 4\n",
    "print(\"Bug found: 'len(celsius)' should be 'len(filtered)'\\n\")\n",
    "\n",
    "# --- Fix ---\n",
    "average = sum(filtered) / len(filtered)\n",
    "print(f\"Fixed result: {average:.1f}\")   # 13.0\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "942df1b0",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(3, 4, 5) is a valid triangle\n",
      "AssertionError: All sides must be positive\n",
      "AssertionError: Side c=10 violates the triangle inequality\n"
     ]
    }
   ],
   "source": [
    "### Exercise: Using Assertions\n",
    "#   Add assert statements to `validate_triangle(a, b, c)` that check:\n",
    "#   1. All three sides are positive (> 0). Use a single assert for all three.\n",
    "#   2. Each side is less than the sum of the other two (triangle inequality).\n",
    "#   Each assert should include a descriptive message.\n",
    "#   Test:\n",
    "#     validate_triangle(3, 4, 5)  → passes silently\n",
    "#     validate_triangle(0, 4, 5)  → AssertionError (side not positive)\n",
    "#     validate_triangle(1, 2, 10) → AssertionError (violates triangle inequality)\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "3f6ba317-92ce-428f-808a-847f74939312",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(3, 4, 5) is a valid triangle\n",
      "AssertionError: All sides must be positive\n",
      "AssertionError: Side c=10 violates the triangle inequality\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "\n",
    "def validate_triangle(a, b, c):\n",
    "    assert a > 0 and b > 0 and c > 0, \"All sides must be positive\"\n",
    "    assert a < b + c, f\"Side a={a} violates the triangle inequality\"\n",
    "    assert b < a + c, f\"Side b={b} violates the triangle inequality\"\n",
    "    assert c < a + b, f\"Side c={c} violates the triangle inequality\"\n",
    "\n",
    "validate_triangle(3, 4, 5)   # passes silently\n",
    "print(\"(3, 4, 5) is a valid triangle\")\n",
    "\n",
    "try:\n",
    "    validate_triangle(0, 4, 5)\n",
    "except AssertionError as e:\n",
    "    print(f\"AssertionError: {e}\")\n",
    "\n",
    "try:\n",
    "    validate_triangle(1, 2, 10)\n",
    "except AssertionError as e:\n",
    "    print(f\"AssertionError: {e}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Type Hints, Static Checking, and Runtime Checking\n",
    "Type hints describe what a function expects, but they are not runtime validation. If you annotate a parameter as `int`, Python will still let someone pass a string unless your code checks for it.\n",
    "\n",
    "```python\n",
    "def double(n: int) -> int:\n",
    "    return n * 2\n",
    "\n",
    "print(double(\"ha\"))   # Python allows this; the result is \"haha\"\n",
    "```\n",
    "\n",
    "There are three different ideas that are easy to confuse:\n",
    "\n",
    "| Technique | When it runs | What it does |\n",
    "|---|---|---|\n",
    "| Type hints | Not enforced by Python itself | Documents expected types |\n",
    "| `mypy` or another static checker | Before the program runs | Reports likely type mistakes |\n",
    "| Runtime checks | While the program runs | Raises errors or handles bad values |\n",
    "\n",
    "A static checker such as `mypy` can warn that a call probably violates the function contract:\n",
    "\n",
    "```bash\n",
    "mypy my_program.py\n",
    "```\n",
    "\n",
    "Runtime checks are regular Python code. Use them when invalid input must be rejected while the program is running.\n",
    "\n"
   ],
   "id": "type-checks-01"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def average(numbers: list[float]) -> float:\n",
    "    if not isinstance(numbers, list):\n",
    "        raise TypeError(f\"numbers must be a list, got {type(numbers).__name__}\")\n",
    "    if len(numbers) == 0:\n",
    "        raise ValueError(\"numbers must not be empty\")\n",
    "    return sum(numbers) / len(numbers)\n",
    "\n",
    "print(average([10.0, 20.0, 30.0]))\n"
   ],
   "id": "type-checks-02"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use type hints to make intent clear. Use tests to verify behavior. Use runtime checks and exceptions only when the program needs to defend itself against invalid data while it is running.\n",
    "\n"
   ],
   "id": "type-checks-03"
  },
  {
   "cell_type": "markdown",
   "id": "ca7eaef3",
   "metadata": {},
   "source": [
    "## Multiple Exception Handling\n",
    "\n",
    "You can handle multiple exception types in a single try/except block using several approaches:\n",
    "\n",
    "1. **Multiple except blocks**: Handle each exception type differently. This is preferred as we want to catch specific exceptions first.\n",
    "2. **Single except with tuple**: Handle multiple types the same way (e.g., `except (ValueError, TypeError):`)\n",
    "3. **Generic except**: Catch any exception (use cautiously because it hides bugs). \n",
    "\n",
    "The better practice is to catch specific exceptions first, and only use except Exception as a last resort for truly unexpected errors.\n",
    "\n",
    "**Generic except** refers to using a bare except or except Exception to catch any exception type:\n",
    "\n",
    "Option 1: bare except (catches absolutely everything, including SystemExit, KeyboardInterrupt)\n",
    "```python\n",
    "try:\n",
    "    risky_code()\n",
    "except:\n",
    "    print(\"Something went wrong\")\n",
    "```\n",
    "Option 2: except Exception (preferred — catches all normal errors but not system exits)\n",
    "```python\n",
    "try:\n",
    "    risky_code()\n",
    "except Exception as e:\n",
    "    print(f\"Error: {type(e).__name__}: {e}\")\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "765f466f",
   "metadata": {},
   "source": [
    "The sample code below demonstrates how multiple exceptions may be handled."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "06a269f3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Testing calculator with error handling:\n",
      "10 add 5 = 15.0\n",
      "----------------------------------------\n",
      "ValueError: could not convert string to float: 'abc'\n",
      "----------------------------------------\n",
      "ZeroDivisionError: Division by zero is not allowed\n",
      "----------------------------------------\n",
      "ValueError: Unknown operation: power\n",
      "----------------------------------------\n"
     ]
    }
   ],
   "source": [
    "# Example 4: Multiple exception handling approaches\n",
    "\n",
    "def calculate_from_strings(num1_str, num2_str, operation):\n",
    "    \"\"\"Perform calculations with comprehensive error handling.\"\"\"\n",
    "    \n",
    "    try:\n",
    "        # Convert strings to numbers\n",
    "        num1 = float(num1_str)\n",
    "        num2 = float(num2_str)\n",
    "        \n",
    "        # Perform the requested operation\n",
    "        if operation == 'add':\n",
    "            result = num1 + num2\n",
    "        elif operation == 'subtract':\n",
    "            result = num1 - num2\n",
    "        elif operation == 'multiply':\n",
    "            result = num1 * num2\n",
    "        elif operation == 'divide':\n",
    "            result = num1 / num2\n",
    "        else:\n",
    "            raise ValueError(f\"Unknown operation: {operation}\")\n",
    "            \n",
    "        return result\n",
    "        \n",
    "    except ValueError as e:\n",
    "        print(f\"ValueError: {e}\")\n",
    "        return None\n",
    "    except ZeroDivisionError:\n",
    "        print(\"ZeroDivisionError: Division by zero is not allowed\")\n",
    "        return None\n",
    "    except Exception as e:      ### general catch-all for unexpected exceptions\n",
    "        print(f\"Unexpected error: {type(e).__name__}: {e}\")\n",
    "        return None\n",
    "\n",
    "# Test different scenarios\n",
    "test_cases = [\n",
    "    (\"10\", \"5\", \"add\"),       # Valid\n",
    "    (\"10\", \"abc\", \"add\"),     # ValueError (invalid number)\n",
    "    (\"10\", \"0\", \"divide\"),    # ZeroDivisionError\n",
    "    (\"10\", \"5\", \"power\"),     # ValueError (unknown operation)\n",
    "]\n",
    "\n",
    "print(\"Testing calculator with error handling:\")\n",
    "for num1, num2, op in test_cases:\n",
    "    result = calculate_from_strings(num1, num2, op)\n",
    "    if result is not None:\n",
    "        print(f\"{num1} {op} {num2} = {result}\")\n",
    "    print(\"-\" * 40)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be25fa11",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "20\n",
      "Index 9 is out of range for a list of length 3\n",
      "None\n",
      "Index must be an integer, got 'str'\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "### Exercise: Multiple Exception Types\n",
    "#   Write `safe_index(lst, i)` that:\n",
    "#   1. Returns lst[i] if the access succeeds.\n",
    "#   2. Catches IndexError (index out of range) — prints a message and returns None.\n",
    "#   3. Catches TypeError (non-integer index) — prints a message and returns None.\n",
    "#   Test:\n",
    "#     safe_index([10, 20, 30], 1)   → 20\n",
    "#     safe_index([10, 20, 30], 9)   → None  (IndexError)\n",
    "#     safe_index([10, 20, 30], 'a') → None  (TypeError)\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "4bf3d7be-fdc8-4035-b29e-0ab11da1e133",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "20\n",
      "Index 9 is out of range for a list of length 3\n",
      "None\n",
      "Index must be an integer, got str\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "\n",
    "def safe_index(lst, i):\n",
    "    try:\n",
    "        return lst[i]\n",
    "    except IndexError:\n",
    "        print(f\"Index {i} is out of range for a list of length {len(lst)}\")\n",
    "        return None\n",
    "    except TypeError:\n",
    "        print(f\"Index must be an integer, got {type(i).__name__}\")\n",
    "        return None\n",
    "\n",
    "print(safe_index([10, 20, 30], 1))    # 20\n",
    "print(safe_index([10, 20, 30], 9))    # message, then None\n",
    "print(safe_index([10, 20, 30], 'a'))  # message, then None\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26ca6efd",
   "metadata": {},
   "source": [
    "## Else and Finally Blocks\n",
    "\n",
    "The try/except statement can include `else` and `finally` blocks for additional control:\n",
    "\n",
    "- **`else` block**: Runs only if NO exception occurred in the try block\n",
    "- **`finally` block**: ALWAYS runs, whether an exception occurred or not\n",
    "\n",
    "**Complete Syntax:**\n",
    "```python\n",
    "try:\n",
    "    # Code that might raise an exception\n",
    "    risky_code()\n",
    "except SpecificException:\n",
    "    # Handle specific exception\n",
    "    handle_error()\n",
    "else:\n",
    "    # Code that runs only if no exception occurred\n",
    "    success_code()\n",
    "finally:\n",
    "    # Code that always runs (cleanup)\n",
    "    cleanup_code()\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "f6729ec3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Processing file: document.txt\n",
      "Successfully opened document.txt\n",
      "File processed successfully!\n",
      "Data length: 24\n",
      "Cleaning up resources...\n",
      "File processor finished\n",
      "Returned: Contents of document.txt\n",
      "==================================================\n",
      "\n",
      "Processing file: missing.txt\n",
      "File error: File does not exist\n",
      "Cleaning up resources...\n",
      "File processor finished\n",
      "Returned: None\n",
      "==================================================\n",
      "\n",
      "Processing file: corrupted.txt\n",
      "Data error: File is corrupted\n",
      "Cleaning up resources...\n",
      "File processor finished\n",
      "Returned: None\n",
      "==================================================\n"
     ]
    }
   ],
   "source": [
    "# Example 5: Using else and finally blocks\n",
    "\n",
    "def file_processor(filename):\n",
    "    \"\"\"Demonstrate else and finally blocks.\"\"\"\n",
    "    print(f\"\\nProcessing file: {filename}\")\n",
    "    \n",
    "    try:\n",
    "        # Simulate file processing\n",
    "        if filename == \"missing.txt\":\n",
    "            raise FileNotFoundError(\"File does not exist\")\n",
    "        elif filename == \"corrupted.txt\":\n",
    "            raise ValueError(\"File is corrupted\")\n",
    "        else:\n",
    "            print(f\"Successfully opened {filename}\")\n",
    "            data = f\"Contents of {filename}\"\n",
    "            \n",
    "    except FileNotFoundError as e:\n",
    "        print(f\"File error: {e}\")\n",
    "        data = None\n",
    "    except ValueError as e:\n",
    "        print(f\"Data error: {e}\")\n",
    "        data = None\n",
    "    else:\n",
    "        # This runs only if no exception occurred\n",
    "        print(\"File processed successfully!\")\n",
    "        print(f\"Data length: {len(data)}\")\n",
    "    finally:\n",
    "        # This always runs\n",
    "        print(\"Cleaning up resources...\")\n",
    "        print(\"File processor finished\")\n",
    "    \n",
    "    return data\n",
    "\n",
    "# Test different scenarios\n",
    "test_files = [\"document.txt\", \"missing.txt\", \"corrupted.txt\"]\n",
    "\n",
    "for filename in test_files:\n",
    "    result = file_processor(filename)\n",
    "    print(f\"Returned: {result}\")\n",
    "    print(\"=\" * 50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "ee87865c",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Safe Division with Cleanup\n",
    "#   Write `safe_divide(a, b)` that:\n",
    "#   1. Returns the result of `a / b`.\n",
    "#   2. If `b` is zero, prints \"Error: cannot divide by zero\" and returns None.\n",
    "#   3. Always prints \"Operation complete.\" in a `finally` block, regardless of outcome.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "6591566b",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Operation complete.\n",
      "5.0\n",
      "Error: cannot divide by zero\n",
      "Operation complete.\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "\n",
    "def safe_divide(a, b):\n",
    "    try:\n",
    "        result = a / b\n",
    "    except ZeroDivisionError:\n",
    "        print(\"Error: cannot divide by zero\")\n",
    "        result = None\n",
    "    finally:\n",
    "        print(\"Operation complete.\")\n",
    "    return result\n",
    "\n",
    "print(safe_divide(10, 2))   # 5.0  (then \"Operation complete.\")\n",
    "print(safe_divide(5, 0))    # error message, then \"Operation complete.\", then None"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1282d16b",
   "metadata": {},
   "source": [
    "## Raising Custom Exceptions\n",
    "\n",
    "Python raises exceptions automatically (e.g., `int(\"abc\")` `# ValueError: invalid literal`). `raise ValueError`, `raise TypeError`, `raise RuntimeError`. This covers 90% of real-world use. Students should know to pick the right built-in type rather than always reaching for Exception.\n",
    "\n",
    "Sometimes, however, you need to raise your own exceptions (in addition to the ones provided by Python) to alert the caller that something is wrong. Note that you raise exceptions to push error handling up to the caller, rather than returning None or a magic value like -1. \n",
    "\n",
    "\n",
    "\n",
    "You use the `raise` statement to raise your own exceptions.\n",
    "\n",
    "**Syntax:**\n",
    "```python\n",
    "raise ExceptionType(\"Error message\")\n",
    "```\n",
    "\n",
    "For example, you may want to raise an exception with a specific alert, which is **detection** work. The caller of the function may catch that exception, which is **handling**. \n",
    "\n",
    "As shown in the code below, the alert is caught by the caller of the function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "8bb4e56f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "b cannot be zero\n"
     ]
    }
   ],
   "source": [
    "def divide(a, b):\n",
    "    if b == 0:\n",
    "        raise ValueError(\"b cannot be zero\")\n",
    "    return a / b\n",
    "\n",
    "try:\n",
    "    result = divide(10, 0)\n",
    "except ValueError as e:\n",
    "    print(e)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6dad194",
   "metadata": {},
   "source": [
    "**Common scenarios for raising exceptions:**\n",
    "- Input validation fails\n",
    "- Business logic violations\n",
    "- Resource constraints\n",
    "- Invalid states"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "fa8cae85",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Valid age: 25\n",
      "Validation failed for -5: Age cannot be negative\n",
      "Validation failed for 200: Age cannot be greater than 150\n",
      "Validation failed for twenty: Age must be a number, got str\n",
      "Valid age: 45.5\n",
      "Validation failed for None: Age must be a number, got NoneType\n"
     ]
    }
   ],
   "source": [
    "# Example 6: Raising custom exceptions\n",
    "\n",
    "def validate_age(age):\n",
    "    \"\"\"Validate age input with custom exceptions.\"\"\"\n",
    "    if not isinstance(age, (int, float)):\n",
    "        raise TypeError(f\"Age must be a number, got {type(age).__name__}\")\n",
    "    \n",
    "    if age < 0:\n",
    "        raise ValueError(\"Age cannot be negative\")\n",
    "    \n",
    "    if age > 150:\n",
    "        raise ValueError(\"Age cannot be greater than 150\")\n",
    "    \n",
    "    print(f\"Valid age: {age}\")\n",
    "    return age\n",
    "\n",
    "# Test age validation\n",
    "test_ages = [25, -5, 200, \"twenty\", 45.5, None]\n",
    "\n",
    "for age in test_ages:\n",
    "    try:\n",
    "        validate_age(age)\n",
    "    except (TypeError, ValueError) as e:\n",
    "        print(f\"Validation failed for {age}: {e}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0afc54f2",
   "metadata": {},
   "source": [
    "*this example below is kept here for you to visit after you learn OOP."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "137216d6",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "186e59ef",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Testing bank account:\n",
      "Deposited $50. New balance: $150\n",
      "Withdrew $30. New balance: $120\n",
      "Banking error: Insufficient funds: balance=$120, requested=$200\n"
     ]
    }
   ],
   "source": [
    "# Example 7: Custom exception classes\n",
    "class InsufficientFundsError(Exception):\n",
    "    \"\"\"Raised when a withdrawal exceeds the current balance.\"\"\"\n",
    "    pass\n",
    "\n",
    "class BankAccount:\n",
    "    \"\"\"Simple bank account with exception handling.\"\"\"\n",
    "    \n",
    "    def __init__(self, initial_balance=0):\n",
    "        if initial_balance < 0:\n",
    "            raise ValueError(\"Initial balance cannot be negative\")\n",
    "        self.balance = initial_balance\n",
    "    \n",
    "    def deposit(self, amount):\n",
    "        if amount <= 0:\n",
    "            raise ValueError(\"Deposit amount must be positive\")\n",
    "        self.balance += amount\n",
    "        print(f\"Deposited ${amount}. New balance: ${self.balance}\")\n",
    "    \n",
    "    def withdraw(self, amount):\n",
    "        if amount <= 0:\n",
    "            raise ValueError(\"Withdrawal amount must be positive\")\n",
    "        if amount > self.balance:\n",
    "            raise InsufficientFundsError(\n",
    "                f\"Insufficient funds: balance=${self.balance}, requested=${amount}\"\n",
    "            )\n",
    "        self.balance -= amount\n",
    "        print(f\"Withdrew ${amount}. New balance: ${self.balance}\")\n",
    "    \n",
    "    def get_balance(self):\n",
    "        return self.balance\n",
    "\n",
    "# Test the bank account\n",
    "print(\"Testing bank account:\")\n",
    "try:\n",
    "    account = BankAccount(100)\n",
    "    account.deposit(50)\n",
    "    account.withdraw(30)\n",
    "    account.withdraw(200)  # This will raise InsufficientFundsError\n",
    "\n",
    "except ValueError as e:\n",
    "    print(f\"Input error: {e}\")\n",
    "except InsufficientFundsError as e:\n",
    "    print(f\"Banking error: {e}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "23709533",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Raising Exceptions\n",
    "#   Write `square_root(x)` that:\n",
    "#   1. Raises a `TypeError` with a descriptive message if x is not a number (int or float).\n",
    "#   2. Raises a `ValueError` with a descriptive message if x < 0.\n",
    "#   3. Otherwise returns x ** 0.5.\n",
    "#   Test:\n",
    "#     square_root(9)       → 3.0\n",
    "#     square_root(-4)      → raises ValueError\n",
    "#     square_root(\"nine\")  → raises TypeError\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "5accaea9",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3.0\n",
      "ValueError: Cannot take square root of negative number: -4\n",
      "TypeError: Expected a number, got str\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "\n",
    "def square_root(x):\n",
    "    if not isinstance(x, (int, float)):\n",
    "        raise TypeError(f\"Expected a number, got {type(x).__name__}\")\n",
    "    if x < 0:\n",
    "        raise ValueError(f\"Cannot take square root of negative number: {x}\")\n",
    "    return x ** 0.5\n",
    "\n",
    "print(square_root(9))        # 3.0\n",
    "\n",
    "try:\n",
    "    square_root(-4)\n",
    "except ValueError as e:\n",
    "    print(f\"ValueError: {e}\")\n",
    "\n",
    "try:\n",
    "    square_root(\"nine\")\n",
    "except TypeError as e:\n",
    "    print(f\"TypeError: {e}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c72d06cc",
   "metadata": {},
   "source": [
    "## Logging\n",
    "\n",
    "The `logging` module provides a flexible framework for recording diagnostic\n",
    "messages during program execution. Unlike `print()`, logging lets you control\n",
    "*which* messages appear, *where* they go, and *how much detail* to include —\n",
    "without changing your code.\n",
    "\n",
    "### Log Levels\n",
    "\n",
    "| Level | Value | When to use |\n",
    "|---|---|---|\n",
    "| `DEBUG` | 10 | Detailed diagnostic info (development only) |\n",
    "| `INFO` | 20 | Confirmation that things are working as expected |\n",
    "| `WARNING` | 30 | Something unexpected; program still running |\n",
    "| `ERROR` | 40 | A more serious problem; something failed |\n",
    "| `CRITICAL` | 50 | A severe error; program may not continue |\n",
    "\n",
    "The default level is `WARNING` — only `WARNING`, `ERROR`, and `CRITICAL`\n",
    "messages appear unless you configure a lower threshold.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2112d48",
   "metadata": {},
   "source": [
    "When configuring logging, you probably would want to save the logs to a file. This can be done in `logging.basicConfig()` using the `filename=` parameter.\n",
    "\n",
    "> **Note — `basicConfig()` in notebooks:** `logging.basicConfig()` is a *no-op* (short for \"no operation\") after the first call in a session: if the root logger already has handlers attached, the call is silently ignored. In a notebook where all cells share the same kernel state, this means only the first `basicConfig()` call in a session takes effect. The code cells below pass `force=True` to `basicConfig()`, which removes and closes any existing handlers before applying the new configuration, so each cell behaves correctly regardless of run order."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "782e3fec",
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "\n",
    "# Configure logging to save to a file\n",
    "# force=True removes and closes any existing handlers so this cell works on re-runs\n",
    "logging.basicConfig(filename='app.log', level=logging.INFO,\n",
    "                    format='%(asctime)s - %(levelname)s - %(message)s',\n",
    "                    force=True)\n",
    "\n",
    "logging.info(\"This is an info message saved to app.log\")\n",
    "logging.warning(\"This is a warning message\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e10fdc6f",
   "metadata": {},
   "source": [
    "If you want to see the logging information in both the console and in the log, you may further configure logging."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "f5a16063",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2026-04-24 01:41:58,652 - INFO - This message will go to both the file and the console.\n"
     ]
    }
   ],
   "source": [
    "import logging\n",
    "\n",
    "# Define the handlers\n",
    "file_handler = logging.FileHandler('app.log')\n",
    "console_handler = logging.StreamHandler()\n",
    "\n",
    "# Configure the root logger\n",
    "# force=True closes and removes any existing handlers before applying this configuration\n",
    "logging.basicConfig(\n",
    "    level=logging.INFO,\n",
    "    format='%(asctime)s - %(levelname)s - %(message)s',\n",
    "    handlers=[file_handler, console_handler],\n",
    "    force=True\n",
    ")\n",
    "\n",
    "logging.info(\"This message will go to both the file and the console.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "51a369a5",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "DEBUG: Reading config file\n",
      "INFO: Server started on port 8080\n",
      "WARNING: Disk space is low\n",
      "ERROR: Failed to connect to database\n",
      "CRITICAL: System is shutting down\n"
     ]
    }
   ],
   "source": [
    "import logging\n",
    "\n",
    "# Show ALL levels (DEBUG and above)\n",
    "logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s', force=True)\n",
    "\n",
    "logging.debug('Reading config file')        # detailed diagnostics\n",
    "logging.info('Server started on port 8080') # normal operations\n",
    "logging.warning('Disk space is low')        # something unexpected\n",
    "logging.error('Failed to connect to database')  # something failed\n",
    "logging.critical('System is shutting down') # severe error\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71f10ddc",
   "metadata": {},
   "source": [
    "### Logging vs `print()`\n",
    "\n",
    "| | `print()` | `logging` |\n",
    "|---|---|---|\n",
    "| Severity levels | No | Yes |\n",
    "| Easy to silence | No (must delete/comment) | Yes (set level higher) |\n",
    "| Timestamps | No | Yes (with format string) |\n",
    "| Write to file | No | Yes |\n",
    "| Production-ready | No | Yes |\n",
    "\n",
    "Best practice: use `print()` for user-facing output and `logging` for\n",
    "diagnostic messages during development and production monitoring.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "f035aa80",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "DEBUG: divide_with_logging called with a=10, b=2\n",
      "INFO: Result: 5.0\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "5.0\n"
     ]
    }
   ],
   "source": [
    "import logging\n",
    "\n",
    "logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s', force=True)\n",
    "\n",
    "def divide_with_logging(a, b):\n",
    "    logging.debug(f'divide_with_logging called with a={a}, b={b}')\n",
    "    if b == 0:\n",
    "        logging.error('Division by zero attempted')\n",
    "        raise ValueError('Cannot divide by zero')\n",
    "    result = a / b\n",
    "    logging.info(f'Result: {result}')\n",
    "    return result\n",
    "\n",
    "print(divide_with_logging(10, 2))   # 5.0\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "ae51e116",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Add Logging\n",
    "#   Write `safe_sqrt(x)` that uses `logging` instead of `print`:\n",
    "#     - DEBUG: log the input value before computing ('safe_sqrt called with x={x}')\n",
    "#     - WARNING: log a warning if x is negative\n",
    "#     - ERROR: raise ValueError if x is negative\n",
    "#     - INFO: log the result before returning\n",
    "#   Test: safe_sqrt(25) → 5.0; safe_sqrt(0) → 0.0; safe_sqrt(-4) → ValueError.\n",
    "import math\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "a1b2c3d4",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "DEBUG: safe_sqrt called with x=25\n",
      "INFO: Result: 5.0\n",
      "DEBUG: safe_sqrt called with x=0\n",
      "INFO: Result: 0.0\n",
      "DEBUG: safe_sqrt called with x=-4\n",
      "WARNING: Negative input: -4\n",
      "ERROR: Cannot take square root of a negative number\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "5.0\n",
      "0.0\n",
      "Caught: Cannot take square root of negative number: -4\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "\n",
    "import math\n",
    "import logging\n",
    "\n",
    "logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s', force=True)\n",
    "\n",
    "def safe_sqrt(x):\n",
    "    logging.debug(f'safe_sqrt called with x={x}')\n",
    "    if x < 0:\n",
    "        logging.warning(f'Negative input: {x}')\n",
    "        logging.error('Cannot take square root of a negative number')\n",
    "        raise ValueError(f'Cannot take square root of negative number: {x}')\n",
    "    result = math.sqrt(x)\n",
    "    logging.info(f'Result: {result}')\n",
    "    return result\n",
    "\n",
    "print(safe_sqrt(25))   # 5.0\n",
    "print(safe_sqrt(0))    # 0.0\n",
    "\n",
    "try:\n",
    "    safe_sqrt(-4)      # raises ValueError\n",
    "except ValueError as e:\n",
    "    print(f'Caught: {e}')\n"
   ]
  }
 ],
 "metadata": {
  "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
}
