{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Use guard clauses to flatten nested conditions\n",
    "\n",
    "**The question.** You have a function whose real work is buried three or four `if` blocks deep because of the preconditions it has to check first. You want the main logic to sit at one indentation level and the preconditions to read as a checklist.\n",
    "\n",
    "Guard clauses — `if <bad>: return …` (or `raise …`) at the top of the function — are the pattern. Each guard handles one invalid case and exits. Anything past the last guard can assume the inputs are good."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Mock objects so the example runs\n",
    "class Customer:\n",
    "    def __init__(self, logged_in): self.logged_in = logged_in\n",
    "class Item:\n",
    "    def __init__(self, price): self.price = price\n",
    "class Order:\n",
    "    def __init__(self, customer, cart, payment_method):\n",
    "        self.customer = customer\n",
    "        self.cart = cart\n",
    "        self.payment_method = payment_method\n",
    "\n",
    "valid = Order(Customer(True), [Item(9.99), Item(14.50)], 'Visa')\n",
    "\n",
    "\n",
    "def process_order(order):\n",
    "    # Guard clauses: handle every bad case up front, then proceed.\n",
    "    if order is None:\n",
    "        return 'Error: no order supplied'\n",
    "    if not order.customer.logged_in:\n",
    "        return 'Error: customer not logged in'\n",
    "    if not order.cart:\n",
    "        return 'Error: cart is empty'\n",
    "    if not order.payment_method:\n",
    "        return 'Error: no payment method'\n",
    "\n",
    "    # The real work, at one indentation level\n",
    "    total = sum(item.price for item in order.cart)\n",
    "    return f'Charged £{total:.2f} via {order.payment_method}'\n",
    "\n",
    "\n",
    "print(process_order(valid))\n",
    "print(process_order(None))\n",
    "print(process_order(Order(Customer(False), [Item(1)], 'Visa')))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: guard with `raise`, not `return`\n",
    "def process_order_raise(order):\n",
    "    if order is None:\n",
    "        raise ValueError('no order supplied')\n",
    "    if not order.customer.logged_in:\n",
    "        raise PermissionError('customer not logged in')\n",
    "    if not order.cart:\n",
    "        raise ValueError('cart is empty')\n",
    "    if not order.payment_method:\n",
    "        raise ValueError('no payment method')\n",
    "    total = sum(item.price for item in order.cart)\n",
    "    return f'Charged £{total:.2f} via {order.payment_method}'\n",
    "\n",
    "print(process_order_raise(valid))\n",
    "try:\n",
    "    process_order_raise(None)\n",
    "except ValueError as e:\n",
    "    print(f'Caught: {e}')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: guards as short-circuit dispatch\n",
    "def describe(value):\n",
    "    if value is None:\n",
    "        return 'no value'\n",
    "    if isinstance(value, bool):                 # must come before int!\n",
    "        return 'a yes/no'\n",
    "    if isinstance(value, (int, float)):\n",
    "        return f'a number: {value}'\n",
    "    if isinstance(value, str):\n",
    "        return f'text of length {len(value)}'\n",
    "    if isinstance(value, (list, tuple, set)):\n",
    "        return f'a collection of {len(value)} item(s)'\n",
    "    return f'some other thing: {type(value).__name__}'\n",
    "\n",
    "for v in [None, True, 42, 3.14, 'hello', [1, 2, 3], {'a': 1}]:\n",
    "    print(f'{v!r:15} -> {describe(v)}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why it works\n",
    "\n",
    "The guarded version reads as *\"here are the things that would stop us, here is what we do otherwise.\"* The nested version forces the reader to assemble that mental model themselves — they have to track each open `if` in their head until they finally reach the real work at the bottom.\n",
    "\n",
    "The win compounds with size. Add a fifth precondition to the nested version and you nest one level deeper (and every existing branch moves). Add it to the guarded version and you add one more `if`/`return` pair at the top. The main logic doesn't move at all.\n",
    "\n",
    "Guards also remove a whole class of \"redundant `else` after return\" noise — once the early branch has returned, there's no need for the `else` that wraps the remainder."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "**Return vs. raise.** Return a string (or a sentinel, or `None`) when the caller is going to branch on the result anyway. Raise when the invalid case is genuinely exceptional and the caller would have to check-and-raise itself. In application code, raising is usually the cleaner signal — the call site catches what it knows how to handle and lets the rest bubble up.\n",
    "\n",
    "**Keep the nesting when:**\n",
    "\n",
    "- The branches are doing genuinely different work, not handling errors — there are two real paths through the function.\n",
    "- The conditions are interdependent in a way that flattening obscures. Sometimes `if A and B and C:` reads better than three separate guards.\n",
    "- The function is tiny. For a three-line function, the nesting isn't hurting anyone.\n",
    "\n",
    "**Watch the guard ordering.** For `isinstance` dispatch, remember that `bool` is a subclass of `int` — check `isinstance(value, bool)` before `isinstance(value, int)` or every boolean will be reported as a number."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Avoid common conditional mistakes](https://agilearn.co.uk/guides/conditional-logic/recipes/avoid-common-conditional-mistakes) — including the redundant-`else`-after-`return` pattern that guard clauses naturally eliminate.\n",
    "- [Choose between if/elif chains, dict dispatch, and match/case](https://agilearn.co.uk/guides/conditional-logic/recipes/choose-between-conditional-patterns) — when guards stop scaling and dispatch tables take over.\n",
    "- [Truthiness rules](https://agilearn.co.uk/guides/conditional-logic/reference/truthiness-rules) — what `if x:` actually tests for.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}