{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "title",
   "metadata": {},
   "source": [
    "# Debugging with pdb\n",
    "\n",
    "In this tutorial, you will learn to use `pdb`, the built-in Python debugger, to step\n",
    "through code, inspect variables, and diagnose problems interactively.\n",
    "\n",
    "**Time commitment:** 15&ndash;20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- Basic Python knowledge (functions, exceptions, control flow)\n",
    "- Familiarity with running Python scripts from the command line\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Explain what `pdb` is and when to use it\n",
    "- Use `breakpoint()` to pause program execution\n",
    "- Navigate code with essential `pdb` commands (`n`, `s`, `c`, `l`, `p`, `q`)\n",
    "- Inspect variables and evaluate expressions in the debugger\n",
    "\n",
    "**Note:** Because `pdb` is an interactive debugger that requires a terminal, the\n",
    "debugging sessions in this tutorial are shown as example output rather than\n",
    "executable code cells. To try the examples yourself, copy the code into a `.py`\n",
    "file and run it from the command line."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "what-is-pdb",
   "metadata": {},
   "source": [
    "## What is `pdb`?\n",
    "\n",
    "`pdb` is the built-in interactive source code debugger in Python. It allows you to:\n",
    "\n",
    "- Pause your program at any point\n",
    "- Step through code one line at a time\n",
    "- Inspect the values of variables\n",
    "- Evaluate arbitrary Python expressions\n",
    "- Set breakpoints to pause at specific locations\n",
    "\n",
    "Unlike `print()` statements or logging, `pdb` lets you explore the state of your\n",
    "program interactively at the exact moment a problem occurs."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "when-to-use",
   "metadata": {},
   "source": [
    "### When to use `pdb`\n",
    "\n",
    "Use the debugger when:\n",
    "\n",
    "- You need to understand the flow of execution in unfamiliar code\n",
    "- A bug is difficult to reproduce or understand from log output alone\n",
    "- You want to inspect complex data structures at runtime\n",
    "- You need to test different values interactively without restarting the program\n",
    "\n",
    "For routine diagnostic output, logging (covered in the previous tutorials) is\n",
    "usually more appropriate."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "breakpoint-section",
   "metadata": {},
   "source": [
    "## The `breakpoint()` function\n",
    "\n",
    "The easiest way to start the debugger is with the built-in `breakpoint()` function,\n",
    "introduced in Python 3.7. When Python reaches a `breakpoint()` call, it pauses\n",
    "execution and opens the `pdb` prompt.\n",
    "\n",
    "Previously, the standard way was to write `import pdb; pdb.set_trace()`. The\n",
    "`breakpoint()` function is simpler and has the same effect."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "breakpoint-example",
   "metadata": {},
   "source": [
    "### Example: Setting a breakpoint\n",
    "\n",
    "Consider the following function that calculates a discount. Save this code in a\n",
    "file called `discount.py`:\n",
    "\n",
    "```python\n",
    "def calculate_discount(price: float, discount_percent: float) -> float:\n",
    "    \"\"\"Calculate the discounted price.\n",
    "\n",
    "    Args:\n",
    "        price: The original price.\n",
    "        discount_percent: The discount as a percentage (for example, 20 for 20%).\n",
    "\n",
    "    Returns:\n",
    "        The price after discount.\n",
    "    \"\"\"\n",
    "    breakpoint()  # Execution will pause here\n",
    "    discount_amount = price * (discount_percent / 100)\n",
    "    final_price = price - discount_amount\n",
    "    return final_price\n",
    "\n",
    "\n",
    "result = calculate_discount(100.0, 20.0)\n",
    "print(\"Final price:\", result)\n",
    "```\n",
    "\n",
    "When you run `python discount.py`, the program pauses at `breakpoint()` and\n",
    "you see the `pdb` prompt:\n",
    "\n",
    "```\n",
    "> /path/to/discount.py(14)calculate_discount()\n",
    "-> discount_amount = price * (discount_percent / 100)\n",
    "(Pdb)\n",
    "```\n",
    "\n",
    "The `->` arrow shows the next line that will execute. You are now in the debugger\n",
    "and can type commands."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "essential-commands",
   "metadata": {},
   "source": [
    "## Essential `pdb` commands\n",
    "\n",
    "Here are the commands you will use most often:\n",
    "\n",
    "| Command | Shortcut | Description |\n",
    "|---------|----------|-------------|\n",
    "| `next` | `n` | Execute the current line, then stop at the next line |\n",
    "| `step` | `s` | Step into a function call |\n",
    "| `continue` | `c` | Continue execution until the next breakpoint |\n",
    "| `list` | `l` | Show the source code around the current line |\n",
    "| `print` | `p` | Print the value of an expression |\n",
    "| `quit` | `q` | Quit the debugger and stop the program |\n",
    "| `where` | `w` | Show the call stack |\n",
    "| `help` | `h` | Show help for a command |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "commands-demo",
   "metadata": {},
   "source": [
    "### Walking through a debugging session\n",
    "\n",
    "Using the `discount.py` example above, here is what a typical debugging session\n",
    "looks like:\n",
    "\n",
    "```\n",
    "$ python discount.py\n",
    "> /path/to/discount.py(14)calculate_discount()\n",
    "-> discount_amount = price * (discount_percent / 100)\n",
    "(Pdb) p price\n",
    "100.0\n",
    "(Pdb) p discount_percent\n",
    "20.0\n",
    "(Pdb) n\n",
    "> /path/to/discount.py(15)calculate_discount()\n",
    "-> final_price = price - discount_amount\n",
    "(Pdb) p discount_amount\n",
    "20.0\n",
    "(Pdb) n\n",
    "> /path/to/discount.py(16)calculate_discount()\n",
    "-> return final_price\n",
    "(Pdb) p final_price\n",
    "80.0\n",
    "(Pdb) c\n",
    "Final price: 80.0\n",
    "```\n",
    "\n",
    "In this session:\n",
    "\n",
    "1. `p price` -- Print the value of `price` (100.0)\n",
    "2. `p discount_percent` -- Print the value of `discount_percent` (20.0)\n",
    "3. `n` -- Execute the current line and move to the next\n",
    "4. `p discount_amount` -- Verify the calculated discount\n",
    "5. `n` -- Execute the next line\n",
    "6. `p final_price` -- Verify the final result\n",
    "7. `c` -- Continue running the program to completion"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "next-vs-step",
   "metadata": {},
   "source": [
    "### `next` versus `step`\n",
    "\n",
    "The difference between `n` (next) and `s` (step) is important:\n",
    "\n",
    "- **`n` (next)** executes the current line completely. If the line contains a\n",
    "  function call, the entire function runs and you stop at the next line in the\n",
    "  current function.\n",
    "- **`s` (step)** steps *into* a function call. If the current line calls a function,\n",
    "  the debugger enters that function and stops at its first line.\n",
    "\n",
    "Use `n` when you trust a function works correctly and want to skip over it.\n",
    "Use `s` when you want to investigate what happens inside a function."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "inspecting-section",
   "metadata": {},
   "source": [
    "## Inspecting variables\n",
    "\n",
    "At the `(Pdb)` prompt, you can do more than just print variables:\n",
    "\n",
    "- **`p expression`** -- Print the result of any Python expression\n",
    "- **`pp expression`** -- Pretty-print (useful for large data structures)\n",
    "- **Type any Python expression** -- You can evaluate any valid Python at the prompt\n",
    "\n",
    "```\n",
    "(Pdb) p price * 2\n",
    "200.0\n",
    "(Pdb) p type(price)\n",
    "<class 'float'>\n",
    "(Pdb) p [x for x in range(5)]\n",
    "[0, 1, 2, 3, 4]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "list-command",
   "metadata": {},
   "source": [
    "### Viewing source code with `list`\n",
    "\n",
    "The `l` (list) command shows the source code around the current line. The current\n",
    "line is marked with `->`. Use `ll` (long list) to see the entire current function.\n",
    "\n",
    "```\n",
    "(Pdb) l\n",
    " 10         discount_percent: The discount as a percentage.\n",
    " 11\n",
    " 12         Returns:\n",
    " 13             The price after discount.\n",
    " 14         \"\"\"\n",
    " 15  ->     discount_amount = price * (discount_percent / 100)\n",
    " 16         final_price = price - discount_amount\n",
    " 17         return final_price\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "call-stack-section",
   "metadata": {},
   "source": [
    "## Navigating the call stack\n",
    "\n",
    "When debugging, you often need to understand how the program reached the current\n",
    "point. The call stack shows the chain of function calls.\n",
    "\n",
    "- **`w` (where)** -- Show the full call stack\n",
    "- **`u` (up)** -- Move up one level in the call stack (to the calling function)\n",
    "- **`d` (down)** -- Move down one level (back towards the current function)\n",
    "\n",
    "Moving up and down the stack lets you inspect variables in different scopes without\n",
    "changing the actual execution point."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "breakpoints-in-pdb",
   "metadata": {},
   "source": [
    "## Setting breakpoints in `pdb`\n",
    "\n",
    "Besides placing `breakpoint()` in your source code, you can set breakpoints\n",
    "interactively from the `(Pdb)` prompt:\n",
    "\n",
    "- **`b lineno`** -- Set a breakpoint at a specific line number\n",
    "- **`b filename:lineno`** -- Set a breakpoint in a specific file\n",
    "- **`b function`** -- Set a breakpoint at the first line of a function\n",
    "- **`b lineno, condition`** -- Set a conditional breakpoint\n",
    "- **`cl` (clear)** -- Remove breakpoints\n",
    "\n",
    "Conditional breakpoints are particularly useful. For example:\n",
    "\n",
    "```\n",
    "(Pdb) b 15, discount_percent > 50\n",
    "Breakpoint 1 at /path/to/discount.py:15\n",
    "```\n",
    "\n",
    "This breakpoint only triggers when `discount_percent` exceeds 50."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "post-mortem-section",
   "metadata": {},
   "source": [
    "## Post-mortem debugging\n",
    "\n",
    "Sometimes you want to investigate the state of your program *after* an exception\n",
    "has occurred. This is called **post-mortem debugging**.\n",
    "\n",
    "There are two ways to do this:\n",
    "\n",
    "1. **`python -m pdb script.py`** -- Run the script under `pdb`. If an unhandled\n",
    "   exception occurs, `pdb` will automatically start at the point of the exception.\n",
    "2. **`pdb.pm()`** -- Start post-mortem debugging in the interactive interpreter\n",
    "   after an exception."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "buggy-function",
   "metadata": {},
   "outputs": [],
   "source": [
    "def find_average(numbers: list[int]) -> float:\n",
    "    \"\"\"Calculate the average of a list of numbers.\n",
    "\n",
    "    Args:\n",
    "        numbers: A list of integers.\n",
    "\n",
    "    Returns:\n",
    "        The arithmetic mean of the numbers.\n",
    "    \"\"\"\n",
    "    total = sum(numbers)\n",
    "    count = len(numbers)\n",
    "    return total / count\n",
    "\n",
    "\n",
    "# This will work\n",
    "print(\"Average of [1, 2, 3]:\", find_average([1, 2, 3]))\n",
    "\n",
    "# This will raise a ZeroDivisionError\n",
    "try:\n",
    "    find_average([])\n",
    "except ZeroDivisionError:\n",
    "    print(\"Caught ZeroDivisionError when averaging an empty list\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "post-mortem-example",
   "metadata": {},
   "source": [
    "If you save the code above (without the `try`/`except`) in a file and run it with\n",
    "`python -m pdb script.py`, the debugger will pause at the `ZeroDivisionError`:\n",
    "\n",
    "```\n",
    "$ python -m pdb script.py\n",
    "> /path/to/script.py(1)<module>()\n",
    "-> def find_average(numbers):\n",
    "(Pdb) c\n",
    "Average of [1, 2, 3]: 2.0\n",
    "Traceback (most recent call last):\n",
    "  ...\n",
    "ZeroDivisionError: division by zero\n",
    "Uncaught exception. Entering post mortem debugging\n",
    "> /path/to/script.py(8)find_average()\n",
    "-> return total / count\n",
    "(Pdb) p total\n",
    "0\n",
    "(Pdb) p count\n",
    "0\n",
    "(Pdb) p numbers\n",
    "[]\n",
    "```\n",
    "\n",
    "Now you can see exactly why the error occurred: `numbers` was an empty list,\n",
    "so `count` was 0, leading to division by zero."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "logging-and-debugging",
   "metadata": {},
   "source": [
    "## Combining logging and debugging\n",
    "\n",
    "Logging and debugging complement each other well:\n",
    "\n",
    "1. **Use logging** to identify *where* a problem occurs (look for unexpected\n",
    "   log messages or missing expected messages)\n",
    "2. **Use `pdb`** to investigate *why* the problem occurs (inspect variables,\n",
    "   step through logic)\n",
    "\n",
    "A common workflow:\n",
    "\n",
    "1. Notice unexpected behaviour\n",
    "2. Check log output to narrow down the problem area\n",
    "3. Add a `breakpoint()` near the suspicious code\n",
    "4. Run the program and inspect the state at the breakpoint\n",
    "5. Fix the bug\n",
    "6. Remove the `breakpoint()` call"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "logging-debugging-example",
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "\n",
    "logger = logging.getLogger(\"order_processor\")\n",
    "\n",
    "\n",
    "def process_order(items: list[dict], tax_rate: float = 0.2) -> float:\n",
    "    \"\"\"Calculate the total cost of an order including tax.\n",
    "\n",
    "    Args:\n",
    "        items: A list of dictionaries with 'name', 'price', and 'quantity' keys.\n",
    "        tax_rate: The tax rate as a decimal (for example, 0.2 for 20%).\n",
    "\n",
    "    Returns:\n",
    "        The total cost including tax.\n",
    "    \"\"\"\n",
    "    logger.info(\"Processing order with %s items\", len(items))\n",
    "    subtotal = 0.0\n",
    "\n",
    "    for item in items:\n",
    "        item_total = item[\"price\"] * item[\"quantity\"]\n",
    "        logger.debug(\"Item %s: %s x %s = %s\", item[\"name\"], item[\"price\"], item[\"quantity\"], item_total)\n",
    "        subtotal += item_total\n",
    "\n",
    "    tax = subtotal * tax_rate\n",
    "    total = subtotal + tax\n",
    "    logger.info(\"Order total: subtotal=%s, tax=%s, total=%s\", subtotal, tax, total)\n",
    "    return total\n",
    "\n",
    "\n",
    "# Example order\n",
    "order = [\n",
    "    {\"name\": \"Widget\", \"price\": 9.99, \"quantity\": 3},\n",
    "    {\"name\": \"Gadget\", \"price\": 24.99, \"quantity\": 1},\n",
    "]\n",
    "\n",
    "total = process_order(order)\n",
    "print(\"Order total: %.2f\" % total)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tips-section",
   "metadata": {},
   "source": [
    "## Tips for effective debugging\n",
    "\n",
    "1. **Start from the error, work backwards.** Read the traceback from bottom to top.\n",
    "   Set your breakpoint just before the line that fails.\n",
    "\n",
    "2. **Form a hypothesis.** Before starting the debugger, think about what you expect\n",
    "   each variable to contain. Then check your assumptions.\n",
    "\n",
    "3. **Use logging to narrow scope.** If the bug is in a large codebase, use log\n",
    "   messages to identify the general area before reaching for `pdb`.\n",
    "\n",
    "4. **Remember to remove breakpoints.** After fixing the bug, always remove any\n",
    "   `breakpoint()` calls from your code.\n",
    "\n",
    "5. **Use `PYTHONBREAKPOINT=0`** to disable all `breakpoint()` calls without\n",
    "   removing them from the code. This is useful in production:\n",
    "\n",
    "   ```bash\n",
    "   PYTHONBREAKPOINT=0 python my_script.py\n",
    "   ```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercises-section",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "These exercises are designed to be completed outside of Jupyter, using a text editor\n",
    "and the command line."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-1",
   "metadata": {},
   "source": [
    "### Exercise 1: Find the bug\n",
    "\n",
    "The following function has a bug. Save it to a file, add a `breakpoint()` call,\n",
    "and use `pdb` to find and fix the problem.\n",
    "\n",
    "```python\n",
    "def celsius_to_fahrenheit(celsius: float) -> float:\n",
    "    \"\"\"Convert a temperature from Celsius to Fahrenheit.\"\"\"\n",
    "    return celsius * 9 / 5 + 32\n",
    "\n",
    "\n",
    "def fahrenheit_to_celsius(fahrenheit: float) -> float:\n",
    "    \"\"\"Convert a temperature from Fahrenheit to Celsius.\"\"\"\n",
    "    return fahrenheit - 32 * 5 / 9  # Bug: operator precedence\n",
    "\n",
    "\n",
    "# This should give us back the original value\n",
    "original = 100.0\n",
    "converted = celsius_to_fahrenheit(original)\n",
    "back = fahrenheit_to_celsius(converted)\n",
    "print(f\"Original: {original}, Converted: {converted}, Back: {back}\")\n",
    "# Expected: Original: 100.0, Converted: 212.0, Back: 100.0\n",
    "```\n",
    "\n",
    "**Hint:** Place a breakpoint inside `fahrenheit_to_celsius` and inspect the\n",
    "intermediate values."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-1-solution",
   "metadata": {},
   "source": [
    "**Solution:** The bug is in `fahrenheit_to_celsius`. The expression\n",
    "`fahrenheit - 32 * 5 / 9` is evaluated as `fahrenheit - ((32 * 5) / 9)` due\n",
    "to operator precedence. The correct version is `(fahrenheit - 32) * 5 / 9`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-2",
   "metadata": {},
   "source": [
    "### Exercise 2: Conditional breakpoints\n",
    "\n",
    "Save the following code to a file and run it under `pdb`\n",
    "(`python -m pdb script.py`). Set a conditional breakpoint that only triggers\n",
    "when `name` is `\"error_item\"`.\n",
    "\n",
    "```python\n",
    "items = [\"apple\", \"banana\", \"error_item\", \"cherry\"]\n",
    "\n",
    "for item in items:\n",
    "    name = item\n",
    "    processed = name.upper()\n",
    "    print(processed)\n",
    "```\n",
    "\n",
    "**Hint:** Use `b lineno, name == \"error_item\"` at the `(Pdb)` prompt."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-3",
   "metadata": {},
   "source": [
    "### Exercise 3: Post-mortem debugging\n",
    "\n",
    "Save the following code to a file and run it with `python -m pdb script.py`.\n",
    "When the exception occurs, inspect the variables to understand why it failed.\n",
    "\n",
    "```python\n",
    "def lookup_user(users: dict, user_id: int) -> str:\n",
    "    \"\"\"Look up a user name by their ID.\"\"\"\n",
    "    return users[user_id]\n",
    "\n",
    "\n",
    "user_database = {1: \"Alice\", 2: \"Bob\", 3: \"Charlie\"}\n",
    "print(lookup_user(user_database, 4))  # KeyError\n",
    "```\n",
    "\n",
    "Use `p users` and `p user_id` at the `(Pdb)` prompt to see why the lookup failed."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "summary",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this tutorial, you learned the following:\n",
    "\n",
    "- `pdb` is the built-in interactive debugger in Python, useful for inspecting\n",
    "  program state and stepping through code\n",
    "- `breakpoint()` is the modern way to set a breakpoint in your code\n",
    "- The essential commands are `n` (next), `s` (step), `c` (continue), `l` (list),\n",
    "  `p` (print), and `q` (quit)\n",
    "- `w` (where) shows the call stack, and `u`/`d` move up and down the stack\n",
    "- Conditional breakpoints let you pause only when a specific condition is met\n",
    "- Post-mortem debugging with `python -m pdb` lets you inspect the state after\n",
    "  an unhandled exception\n",
    "- Logging and debugging complement each other: use logging to find the general\n",
    "  area of a problem, then use `pdb` to investigate the details\n",
    "\n",
    "Congratulations on completing all four tutorials! You now have a solid foundation\n",
    "in both logging and debugging with Python. For more advanced topics, explore the\n",
    "[Recipes](https://agilearn.co.uk/guides/logging-and-debugging/recipes/index) and [Reference](https://agilearn.co.uk/guides/logging-and-debugging/reference/index) sections."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}