{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Choose between `if`/`elif` chains, dict dispatch, and `match`/`case`\n",
    "\n",
    "**The question.** You have a function that branches on a single value — an HTTP status code, an event type, a command name — and you need to pick one of three tools: an `if`/`elif` chain, a dict of handlers, or a `match`/`case` block. Which one does this branching deserve?\n",
    "\n",
    "The short answer: start with `if`/`elif`. Reach for dict dispatch when you have many branches that are each a simple lookup-and-call. Reach for `match` when the branches depend on **shape** (fields, nesting) as well as value, or when destructuring would otherwise clutter every branch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The problem: route an HTTP response (a dict with `status` and `body`)\n",
    "ok_response      = {'status': 200, 'body': {'data': [1, 2, 3]}}\n",
    "created_response = {'status': 201, 'body': {'id': 42}}\n",
    "not_found        = {'status': 404, 'body': {'error': 'missing'}}\n",
    "server_error     = {'status': 500, 'body': {'error': 'internal'}}\n",
    "unknown          = {'status': 999, 'body': {}}\n",
    "\n",
    "\n",
    "# The default: if/elif. Every branch is free-form; nothing to set up.\n",
    "def route(response):\n",
    "    status = response['status']\n",
    "    if status == 200:\n",
    "        return f\"OK — got {len(response['body']['data'])} items\"\n",
    "    elif status == 201:\n",
    "        return f\"Created with id {response['body']['id']}\"\n",
    "    elif status == 404:\n",
    "        return 'Not found — fall back to cache'\n",
    "    elif status == 500:\n",
    "        return 'Server error — back off and retry'\n",
    "    else:\n",
    "        return f'Unexpected status {status}'\n",
    "\n",
    "\n",
    "for r in [ok_response, created_response, not_found, server_error, unknown]:\n",
    "    print(route(r))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: dict dispatch — adding a case is one line\n",
    "def handle_ok(r):          return f\"OK — got {len(r['body']['data'])} items\"\n",
    "def handle_created(r):     return f\"Created with id {r['body']['id']}\"\n",
    "def handle_not_found(_):   return 'Not found — fall back to cache'\n",
    "def handle_server_error(_): return 'Server error — back off and retry'\n",
    "def handle_unknown(r):     return f\"Unexpected status {r['status']}\"\n",
    "\n",
    "ROUTES = {\n",
    "    200: handle_ok,\n",
    "    201: handle_created,\n",
    "    404: handle_not_found,\n",
    "    500: handle_server_error,\n",
    "}\n",
    "\n",
    "def route_dict(response):\n",
    "    handler = ROUTES.get(response['status'], handle_unknown)\n",
    "    return handler(response)\n",
    "\n",
    "for r in [ok_response, created_response, not_found, server_error, unknown]:\n",
    "    print(route_dict(r))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: match/case — branch on shape and destructure at once\n",
    "def route_match(response):\n",
    "    match response:\n",
    "        case {'status': 200, 'body': {'data': data}}:\n",
    "            return f'OK — got {len(data)} items'\n",
    "        case {'status': 201, 'body': {'id': id}}:\n",
    "            return f'Created with id {id}'\n",
    "        case {'status': 404}:\n",
    "            return 'Not found — fall back to cache'\n",
    "        case {'status': 500}:\n",
    "            return 'Server error — back off and retry'\n",
    "        case {'status': status}:\n",
    "            return f'Unexpected status {status}'\n",
    "\n",
    "for r in [ok_response, created_response, not_found, server_error, unknown]:\n",
    "    print(route_match(r))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why each form earns its place\n",
    "\n",
    "**`if`/`elif`** is the baseline because it assumes nothing. Each branch can return, raise, call, or log — whatever the case demands. The reader sees, top to bottom, exactly what runs. The cost is linear-in-branches repetition of `status == …` and a growing wall of `elif`.\n",
    "\n",
    "**Dict dispatch** replaces the branching with a lookup. Each case becomes a named handler, and the routing logic collapses into a data structure — `ROUTES.get(key, default)`. Adding a case is one line in the dict. That's why plugin systems, command parsers, and event dispatchers gravitate to this pattern: they can be extended at runtime (`ROUTES[new_key] = new_handler`) and introspected (`ROUTES.keys()` tells you what's supported).\n",
    "\n",
    "**`match`/`case`** earns its place when branches are keyed on *shape*, not just value. The pattern `{'status': 200, 'body': {'data': data}}` matches the structure and binds `data` in one step. The equivalent `if` chain has to `response['body']['data']` every time, and the equivalent dict dispatch would need each handler to dig through the response itself. `match` lets the branch condition and the destructuring be the same expression."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "| Use this… | When… | Costs |\n",
    "| --- | --- | --- |\n",
    "| **`if`/`elif`** | A handful of branches; each branch does distinct work; conditions go beyond equality (`if score > 0.8`). | Scales poorly past a dozen cases; repeats the test expression. |\n",
    "| **Dict dispatch** | Many branches keyed on one value; each branch is a callable; you want runtime-extensible routing. | Each case becomes a separate function (overkill for one-liners); logic spread across the handler table and its handlers. |\n",
    "| **`match`/`case`** | Branches depend on shape as well as value; you want to destructure dicts, dataclasses, or tuples. | Python 3.10+ only; pattern syntax has its own gotchas (capture-vs-compare; `case 4 \\| 5:` matches the literals 4 or 5, not \"any 4xx\"). |\n",
    "\n",
    "In practice, the choice depends on what's likely to change. If branches grow in number but keep the same shape, dict dispatch wins. If new branches need new shapes (different fields, different types), `match` wins. If branches are heterogeneous one-offs, `if`/`elif` is fine — and you can always combine them: `match` to peel off the outer shape, then a dict inside one branch for fine-grained routing."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [`match`/`case` syntax](https://agilearn.co.uk/guides/conditional-logic/reference/match-case-syntax) — every pattern type at a glance.\n",
    "- [Structural pattern matching in context](https://agilearn.co.uk/guides/conditional-logic/concepts/structural-pattern-matching-in-context) — when `match` earns its place and when it doesn't.\n",
    "- [Use guard clauses to flatten nested conditions](https://agilearn.co.uk/guides/conditional-logic/recipes/use-guard-clauses) — the refactor that often comes before any of these dispatch styles.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}