{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Pattern matching with `match`/`case`\n\nIn this tutorial, you will learn to use `match`/`case`, Python's structural pattern matching syntax (introduced in Python 3.10). You will see when it makes code clearer than `if`/`elif`, and when it doesn't.\n\n**Time commitment:** 15–20 minutes\n\n**Prerequisites:**\n\n- Completion of [If statements](https://agilearn.co.uk/guides/conditional-logic/learn/01-if-statements)\n- Completion of [Boolean operators and truthiness](https://agilearn.co.uk/guides/conditional-logic/learn/02-boolean-operators-and-truthiness)\n- Comfort with Python tuples, lists, and dictionaries\n\n## Learning objectives\n\nBy the end of this tutorial, you will be able to:\n\n- Read and write `match`/`case` statements\n- Use literal, capture, sequence, mapping, and class patterns\n- Combine patterns with `|` (OR) and `if` guards\n- Recognise the \"capture vs compare\" trap\n- Decide when `match` is clearer than `if`/`elif`"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## The motivation\n\nImagine you're handling events from a UI. Each event is a dict with a `\"type\"` key and other fields that depend on the type. The `if`/`elif` version looks like this:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def handle_if(event):\n    if event[\"type\"] == \"click\":\n        print(f\"clicked at ({event['x']}, {event['y']})\")\n    elif event[\"type\"] == \"keypress\":\n        print(f\"key pressed: {event['key']}\")\n    elif event[\"type\"] == \"scroll\":\n        print(f\"scrolled by {event['delta']}\")\n    else:\n        print(f\"unknown event: {event}\")\n\nhandle_if({\"type\": \"click\", \"x\": 10, \"y\": 20})\nhandle_if({\"type\": \"keypress\", \"key\": \"Enter\"})"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "It works, but each branch repeats the same shape: check the `type`, then dig into specific keys. Pattern matching lets you describe the shape and the destructuring in one go."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def handle_match(event):\n    match event:\n        case {\"type\": \"click\", \"x\": x, \"y\": y}:\n            print(f\"clicked at ({x}, {y})\")\n        case {\"type\": \"keypress\", \"key\": key}:\n            print(f\"key pressed: {key}\")\n        case {\"type\": \"scroll\", \"delta\": delta}:\n            print(f\"scrolled by {delta}\")\n        case _:\n            print(f\"unknown event: {event}\")\n\nhandle_match({\"type\": \"click\", \"x\": 10, \"y\": 20})\nhandle_match({\"type\": \"keypress\", \"key\": \"Enter\"})"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "The `match` version reads as a list of shapes the function knows how to handle. The keys mentioned in each pattern are also the names you use inside the body. There's no separate \"check, then unpack\" step."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## The basic shape\n\n```python\nmatch subject:\n    case pattern_1:\n        ...\n    case pattern_2:\n        ...\n    case _:\n        ...   # wildcard — matches anything\n```\n\nPython evaluates `subject` once, then tries each `case` in order. The first matching pattern wins. There is no fall-through (unlike `switch` in C-family languages), so you don't need `break`."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Literal patterns\n\nThe simplest patterns match exact values: numbers, strings, booleans, and `None`."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def describe_status(code):\n    match code:\n        case 200:\n            return \"OK\"\n        case 404:\n            return \"Not Found\"\n        case 500:\n            return \"Server Error\"\n        case _:\n            return f\"unhandled status {code}\"\n\nprint(describe_status(200))\nprint(describe_status(404))\nprint(describe_status(418))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "For these cases, `match` doesn't buy you much over `if`/`elif`. Where it earns its place is when the patterns describe **shape** as well as value."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Capture patterns\n\nA bare name in a `case` pattern doesn't compare — it **captures**. The name is bound to whatever the subject is."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def describe_pair(pair):\n    match pair:\n        case (0, 0):\n            return \"origin\"\n        case (x, y):\n            return f\"point at ({x}, {y})\"\n\nprint(describe_pair((0, 0)))\nprint(describe_pair((3, 4)))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "In the second case, `x` and `y` are not pre-existing variables — they're being created by the match. After the match, you can use them inside the body.\n\nThe wildcard `_` is a special capture: it matches anything but binds nothing."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Sequence patterns\n\nSequence patterns match tuples and lists. You can match by length or by prefix-and-rest."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def describe_items(items):\n    match items:\n        case []:\n            return \"empty\"\n        case [single]:\n            return f\"one item: {single}\"\n        case [first, second]:\n            return f\"two items: {first}, {second}\"\n        case [first, *rest]:\n            return f\"{first} and {len(rest)} more\"\n\nprint(describe_items([]))\nprint(describe_items([\"apple\"]))\nprint(describe_items([\"apple\", \"pear\"]))\nprint(describe_items([\"apple\", \"pear\", \"plum\", \"fig\"]))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Note that **strings are not treated as sequences** for matching purposes. `case [a, b, c]:` matches a list or tuple of three items, not the string `\"abc\"`."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Mapping patterns\n\nMapping patterns match dicts. Only the keys you mention need to be present — extra keys are ignored."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def describe_user(user):\n    match user:\n        case {\"name\": name, \"admin\": True}:\n            return f\"{name} is an admin\"\n        case {\"name\": name, \"guest\": True}:\n            return f\"{name} is a guest\"\n        case {\"name\": name}:\n            return f\"{name} is a regular user\"\n        case _:\n            return \"unknown user record\"\n\nprint(describe_user({\"name\": \"Ada\", \"admin\": True}))\nprint(describe_user({\"name\": \"Ben\", \"guest\": True}))\nprint(describe_user({\"name\": \"Cas\", \"joined\": \"2024-01-15\"}))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Order matters: the more specific patterns come first, so the catch-all `{\"name\": name}` doesn't grab the admin and guest cases."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Class patterns\n\nClass patterns let you match by type and bind attributes. They're especially useful with `dataclasses`."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float\n\n@dataclass\nclass Circle:\n    centre: Point\n    radius: float\n\n\ndef describe_shape(shape):\n    match shape:\n        case Point(x=0, y=0):\n            return \"origin\"\n        case Point(x=x, y=y):\n            return f\"point at ({x}, {y})\"\n        case Circle(centre=Point(x=cx, y=cy), radius=r):\n            return f\"circle at ({cx}, {cy}) with radius {r}\"\n        case _:\n            return \"unknown shape\"\n\n\nprint(describe_shape(Point(0, 0)))\nprint(describe_shape(Point(3, 4)))\nprint(describe_shape(Circle(centre=Point(1, 2), radius=5)))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Patterns can nest: `Circle(centre=Point(x=cx, y=cy), ...)` reaches into the `centre` attribute and matches a `Point` inside it. This is where `match` really starts to do work `if`/`elif` can't easily replicate."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## OR patterns\n\nCombine alternatives with `|`. The case matches if any alternative matches."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def kind_of_day(day):\n    match day:\n        case \"Saturday\" | \"Sunday\":\n            return \"weekend\"\n        case \"Monday\" | \"Tuesday\" | \"Wednesday\" | \"Thursday\" | \"Friday\":\n            return \"weekday\"\n        case _:\n            return \"unknown day\"\n\nprint(kind_of_day(\"Saturday\"))\nprint(kind_of_day(\"Wednesday\"))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "All the alternatives in an OR pattern must bind the same names (or none). You can't have one branch capture `x` and another capture `y` — Python wouldn't know which name to bind."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Guarded patterns\n\nA pattern can be followed by `if guard:` — an extra condition that has to hold for the case to match."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def describe_pair(pair):\n    match pair:\n        case (x, y) if x == y:\n            return f\"on the diagonal at {x}\"\n        case (x, 0):\n            return f\"on the x-axis at x={x}\"\n        case (0, y):\n            return f\"on the y-axis at y={y}\"\n        case (x, y):\n            return f\"point at ({x}, {y})\"\n\nprint(describe_pair((3, 3)))\nprint(describe_pair((5, 0)))\nprint(describe_pair((0, 7)))\nprint(describe_pair((1, 2)))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "The guard runs only after the pattern structure has matched. It's the right tool when \"the shape is right, but I also need to compare the captured values\"."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## The capture trap\n\nThis is the most common stumbling block with `match`. A bare name in a `case` pattern doesn't compare against the existing variable of that name — it **captures**, binding the name to whatever the subject is.\n\nTry running the cell below."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "STATUS_OK = 200\n\ndef check_status(code):\n    match code:\n        case STATUS_OK:\n            return \"all good\"\n\nprint(check_status(200))   # all good — as expected\nprint(check_status(500))   # also \"all good\" — NOT what you want!"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Both calls returned `\"all good\"`. Why?\n\nThe `STATUS_OK` in `case STATUS_OK:` is a **capture pattern**, not a value comparison. Python sees a bare name and binds it to whatever `code` is — overwriting the local `STATUS_OK` in the process. The pattern always matches.\n\nPython actually has a guard against the most obvious form of this. If you add a second case after a bare-name capture, the compiler catches it as a `SyntaxError`:\n\n```python\nmatch code:\n    case STATUS_OK:        # captures everything...\n        return \"all good\"\n    case _:                # ...so this is unreachable\n        return \"something else\"\n# SyntaxError: name capture 'STATUS_OK' makes remaining patterns unreachable\n```\n\nThat helpful error doesn't fire when the capture is the only case (as above), or when a guard hides the unreachability. The trap is real; the compiler only catches the most obvious shape of it.\n\nTo compare against a constant, use a **dotted name** — Python treats those as value patterns, not captures:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "class Status:\n    OK = 200\n    NOT_FOUND = 404\n\ndef check_status(code):\n    match code:\n        case Status.OK:           # dotted name — value comparison\n            return \"all good\"\n        case Status.NOT_FOUND:\n            return \"not found\"\n        case _:\n            return \"something else\"\n\nprint(check_status(200))\nprint(check_status(404))\nprint(check_status(500))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Or use a guard:\n\n```python\ncase x if x == STATUS_OK:\n    return \"all good\"\n```\n\nOr just write the value directly: `case 200:`. The dotted-name rule is the cleanest fix when you're working with named constants."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Exercise: rewrite an event handler\n\nHere is an `if`/`elif` version of an event handler. Rewrite it using `match`/`case`."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def handle_event_if(event):\n    if isinstance(event, dict) and event.get(\"type\") == \"message\":\n        if \"from\" in event and \"text\" in event:\n            return f\"{event['from']}: {event['text']}\"\n    if isinstance(event, dict) and event.get(\"type\") == \"join\":\n        if \"user\" in event:\n            return f\"{event['user']} joined\"\n    if isinstance(event, dict) and event.get(\"type\") == \"leave\":\n        if \"user\" in event:\n            return f\"{event['user']} left\"\n    return \"unknown event\"\n\n\n# Tests\nprint(handle_event_if({\"type\": \"message\", \"from\": \"Ada\", \"text\": \"hello\"}))\nprint(handle_event_if({\"type\": \"join\", \"user\": \"Ben\"}))\nprint(handle_event_if({\"type\": \"leave\", \"user\": \"Cas\"}))\nprint(handle_event_if({\"type\": \"unknown\"}))"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "# Write your match-based version here\ndef handle_event_match(event):\n    pass\n\n\n# Tests\nprint(handle_event_match({\"type\": \"message\", \"from\": \"Ada\", \"text\": \"hello\"}))\nprint(handle_event_match({\"type\": \"join\", \"user\": \"Ben\"}))\nprint(handle_event_match({\"type\": \"leave\", \"user\": \"Cas\"}))\nprint(handle_event_match({\"type\": \"unknown\"}))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Solution\n\nThe mapping patterns let you check the type *and* destructure the relevant fields in one pattern:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "def handle_event_match(event):\n    match event:\n        case {\"type\": \"message\", \"from\": sender, \"text\": text}:\n            return f\"{sender}: {text}\"\n        case {\"type\": \"join\", \"user\": user}:\n            return f\"{user} joined\"\n        case {\"type\": \"leave\", \"user\": user}:\n            return f\"{user} left\"\n        case _:\n            return \"unknown event\"\n\n\nprint(handle_event_match({\"type\": \"message\", \"from\": \"Ada\", \"text\": \"hello\"}))\nprint(handle_event_match({\"type\": \"join\", \"user\": \"Ben\"}))\nprint(handle_event_match({\"type\": \"leave\", \"user\": \"Cas\"}))\nprint(handle_event_match({\"type\": \"unknown\"}))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "Notice how the four branches each describe the shape of the event they handle — there's no separate `isinstance` check or `event[\"...\"]` indexing. The match version is shorter and the intent is more visible."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## When to reach for `match` (and when not to)\n\n`match` shines when:\n\n- You're dispatching on **structured data** — dicts with type fields, dataclass instances, parsed messages, AST nodes\n- You'd otherwise nest `isinstance` checks and attribute access\n- The set of cases is **closed** and enumerable at the point of the match\n\nIt's not the right tool when:\n\n- You're checking a **single value** against a few options — `if x == 1 ... elif x == 2:` is perfectly clear\n- Conditions involve **arithmetic or method calls** on the subject — `if score >= 0.8:` isn't something `match` patterns express naturally\n- You want to compare against **runtime values bound to local names** (because of the capture trap above)\n\nThe [Choose between if/elif chains, dict dispatch, and match/case](https://agilearn.co.uk/guides/conditional-logic/recipes/choose-between-conditional-patterns) recipe walks through the trade-offs with worked examples."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Summary\n\nIn this tutorial, you learned how to:\n\n- Write `match`/`case` statements to dispatch on structure as well as value\n- Use literal, capture, sequence, mapping, and class patterns\n- Combine patterns with `|` and refine them with `if` guards\n- Avoid the capture-vs-compare trap by using dotted names for constants\n- Recognise the situations where `match` is clearer than `if`/`elif`, and the situations where it isn't\n\n## What is next\n\nThat's all three Learn tutorials in this guide. To go deeper, explore the other sections:\n\n- **[Recipes](https://agilearn.co.uk/guides/conditional-logic/recipes)** — practical conditional patterns: guard clauses, choosing between dispatch styles, and avoiding the common mistakes\n- **[Reference](https://agilearn.co.uk/guides/conditional-logic/reference)** — operators, truthiness rules, and the full `match`/`case` syntax\n- **[Concepts](https://agilearn.co.uk/guides/conditional-logic/concepts)** — why truthiness works the way it does, and where structural pattern matching fits"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}