{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Use breakpoints effectively\n",
    "\n",
    "**The question.** A bug isn't obvious from the code or the logs and you want to step through a specific piece of it — inspect a variable mid-loop, watch what happens inside a called function, pause only when a certain record triggers the failure. Adding `print` statements everywhere is slow and noisy.\n",
    "\n",
    "The answer: drop a `breakpoint()` call where you want to pause. Python opens `pdb`; you inspect with `p`, step with `n`/`s`, continue with `c`, and resume normally. No imports, no magic — and you can disable every `breakpoint()` in one go with `PYTHONBREAKPOINT=0`.\n",
    "\n",
    "(Because pdb is interactive, the canonical answer below just *sets up* the scenario. To actually step through, copy the code into a `.py` file and run `python -m pdb`, or leave the `breakpoint()` line in and run normally.)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The minimal shape: a function that fails on certain input, and a commented-out\n",
    "# breakpoint() you can enable to step through.\n",
    "def parse_config(text: str) -> dict:\n",
    "    '''Parse simple key=value lines. Fails loudly on malformed lines.'''\n",
    "    config = {}\n",
    "    for line in text.strip().split('\\n'):\n",
    "        # Enable the next line to drop into pdb at each line:\n",
    "        # breakpoint()\n",
    "        key, value = line.split('=')     # <-- will ValueError on bad input\n",
    "        config[key.strip()] = value.strip()\n",
    "    return config\n",
    "\n",
    "\n",
    "# Good input — runs fine.\n",
    "print(parse_config('host=localhost\\nport=8080'))\n",
    "\n",
    "# Bad input — unpacks wrong, raises ValueError.\n",
    "# Uncomment to see the exception in this notebook; run with `python -m pdb`\n",
    "# to land in the post-mortem debugger at the failing line.\n",
    "try:\n",
    "    parse_config('host=localhost\\nthis line has no equals sign')\n",
    "except ValueError as exc:\n",
    "    print(f'caught: {exc}')\n",
    "\n",
    "# Commands inside pdb (once you've run with -m pdb or enabled breakpoint()):\n",
    "#   p line        # print the offending line's value\n",
    "#   p line.split('=')   # see that split returns only ['this line...']\n",
    "#   n            # step to the next line\n",
    "#   s            # step into a function call\n",
    "#   c            # continue until the next breakpoint or end\n",
    "#   w            # show the call stack ('where')\n",
    "#   q            # quit\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: conditional and post-mortem debugging\n",
    "\n",
    "Two patterns to know beyond the basic `breakpoint()`.\n",
    "\n",
    "**Conditional breakpoint** — for loops where you only care about specific iterations:\n",
    "\n",
    "```python\n",
    "for i, item in enumerate(items):\n",
    "    if item['status'] == 'error':\n",
    "        breakpoint()          # pause only on error rows\n",
    "    process(item)\n",
    "```\n",
    "\n",
    "**Post-mortem** — run with `python -m pdb script.py`. On any unhandled exception, pdb opens at the failing frame with locals intact. Useful when you don't know where the bug is.\n",
    "\n",
    "```\n",
    "$ python -m pdb config_parser.py\n",
    "> config_parser.py(1)<module>()\n",
    "-> import logging\n",
    "(Pdb) c\n",
    "ValueError: not enough values to unpack (expected 2, got 1)\n",
    "Uncaught exception. Entering post mortem debugging\n",
    "> config_parser.py(9)parse_config()\n",
    "-> key, value = line.split('=')\n",
    "(Pdb) p line\n",
    "'this line has no equals sign'\n",
    "(Pdb) p line.split('=')\n",
    "['this line has no equals sign']\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: essential pdb commands\n",
    "\n",
    "The short list. Everything here works inside any pdb prompt, whether you got there via `breakpoint()` or `python -m pdb`.\n",
    "\n",
    "| Command | What it does |\n",
    "| --- | --- |\n",
    "| `p expr` / `pp expr` | Print / pretty-print an expression |\n",
    "| `n` | Step over the current line |\n",
    "| `s` | Step into a function call |\n",
    "| `r` | Continue until the current function returns |\n",
    "| `c` | Continue to the next breakpoint or program end |\n",
    "| `l` / `ll` | List source around current line / entire function |\n",
    "| `w` | Show the call stack ('where am I?') |\n",
    "| `u` / `d` | Move up / down a frame in the stack |\n",
    "| `b N` | Set a breakpoint at line N |\n",
    "| `b N, cond` | Set a conditional breakpoint |\n",
    "| `cl N` | Clear breakpoint N |\n",
    "| `q` | Quit the debugger |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why this works\n",
    "\n",
    "`breakpoint()` is a built-in that hands control to whichever debugger the `PYTHONBREAKPOINT` environment variable names — `pdb.set_trace` by default, `ipdb.set_trace` if you prefer that, or nothing at all (`PYTHONBREAKPOINT=0` disables every call without editing the code). That makes it ideal for leaving strategically-placed breakpoints in source during active debugging: disable them in CI, enable them locally.\n",
    "\n",
    "Running with `python -m pdb script.py` gives you *post-mortem* debugging for free. If the script raises an unhandled exception, pdb opens at the failing frame with every local variable still alive. Much faster than sprinkling `print` statements after the fact.\n",
    "\n",
    "Inside pdb, the commands you'll actually use are: `p expr` to inspect, `n` to step over, `s` to step into, `c` to continue, `w` to see the call stack, `q` to quit. That's the 80 % toolkit — the rest of the command set is useful but rarely needed.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "`print` and `logging` are still valuable — `print` for exploratory throwaway work, `logging` for persistent diagnostics you'll want again. `breakpoint()` is for the cases where the problem needs a live inspection session: 'why does this variable hold that value at this point?'.\n",
    "\n",
    "For functions called many times, guard the breakpoint with an `if`: `if item['status'] == 'error': breakpoint()`. Much faster than typing `c` through hundreds of iterations of the normal case.\n",
    "\n",
    "Remember to remove `breakpoint()` calls before committing. A `grep -rn 'breakpoint()'` in a pre-commit hook or CI check is a tiny safeguard with a big payoff — a `breakpoint()` merged to main will hang any automated invocation that isn't expecting an interactive session.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Avoid common logging mistakes](https://agilearn.co.uk/guides/logging-and-debugging/recipes/avoid-common-logging-mistakes) — when `logger.exception` beats a manual step-through.\n",
    "- [Configure logging for a project](https://agilearn.co.uk/guides/logging-and-debugging/recipes/configure-logging-for-a-project) — logging as the always-on counterpart to interactive debugging.\n",
    "- [pdb commands reference](https://agilearn.co.uk/guides/logging-and-debugging/reference/pdb-commands-reference) — every command in one place.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}