{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Work with `Optional` values\n",
    "\n",
    "**The question.** You have a function that might return `X` or might return `None` — a lookup, a parse, a cache hit — and the type-checker insists you handle both cases before you can treat the result as an `X`. You want a clean pattern for narrowing `X | None` to `X`, plus the common pitfalls around defaults.\n",
    "\n",
    "The canonical pattern is **guard with `is not None`** (not `if x:` — that hits `0`, `\"\"`, `[]` too). Either inline the guard or early-return; the type-checker narrows the type inside the guard or past the return."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def find_email(user_id: int) -> str | None:\n",
    "    if user_id == 1:\n",
    "        return 'alice@example.com'\n",
    "    return None\n",
    "\n",
    "\n",
    "# Pattern 1: inline if — narrows inside the block\n",
    "email = find_email(1)\n",
    "if email is not None:\n",
    "    # type-checker knows email is str here — .upper() is safe\n",
    "    print('inline:', email.upper())\n",
    "\n",
    "\n",
    "# Pattern 2: early return — narrows past the guard\n",
    "def send_welcome(user_id: int) -> None:\n",
    "    email = find_email(user_id)\n",
    "    if email is None:\n",
    "        return                           # past this line, email: str\n",
    "    print(f'Sending welcome to {email.upper()}')\n",
    "\n",
    "send_welcome(1)\n",
    "send_welcome(99)     # returns silently\n",
    "\n",
    "\n",
    "# Pattern 3: default via ternary — clearer than `or` for the None case\n",
    "def with_timeout(timeout: float | None = None) -> float:\n",
    "    # ternary, not `or`: 0.0 is valid here, `or` would wrongly replace it\n",
    "    return timeout if timeout is not None else 30.0\n",
    "\n",
    "print('defaults:', with_timeout(), with_timeout(5.0), with_timeout(0.0))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: dict .get() with defaults — narrows automatically\n",
    "data: dict[str, str] = {'name': 'Alice'}\n",
    "\n",
    "# With a default: return type is str (not str | None)\n",
    "name  = data.get('name',  'unknown')\n",
    "email = data.get('email', 'no email')\n",
    "\n",
    "# Without a default: return type is str | None\n",
    "maybe_phone = data.get('phone')\n",
    "if maybe_phone is not None:\n",
    "    print('phone:', maybe_phone)\n",
    "\n",
    "print(name, '|', email, '|', maybe_phone)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: return empty collections, not None, when callers will iterate\n",
    "def find_matches_bad(pattern: str, haystack: list[str]) -> list[str] | None:\n",
    "    matches = [s for s in haystack if pattern in s]\n",
    "    return matches if matches else None   # forces every caller to check for None\n",
    "\n",
    "def find_matches(pattern: str, haystack: list[str]) -> list[str]:\n",
    "    return [s for s in haystack if pattern in s]   # empty list means 'no matches'\n",
    "\n",
    "# With the good version, the loop just doesn't run — no None check needed\n",
    "for m in find_matches('zz', ['a', 'b']):\n",
    "    print(m)\n",
    "print('done')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why it works\n",
    "\n",
    "`Optional[X]` and `X | None` are the same thing — a union with `None`. The type-checker follows **narrowing**: if you prove the value isn't `None` (via `is not None`, `isinstance`, or `assert x is not None`), the inferred type inside the guard collapses to `X`. Past an early return, the same thing happens — the checker knows that if execution made it past the guard, the value can't be `None`.\n",
    "\n",
    "`is not None` is the right test because it matches exactly one value. Truthiness (`if x:`) is false for `None`, `0`, `\"\"`, `[]`, `{}`, and `False` — any of which might be a legitimate non-None value for your type. The ternary defaulting pattern (`x if x is not None else default`) sidesteps this trap entirely.\n",
    "\n",
    "The walrus operator (`if (user := find_user(1)) is not None and (email := user.email) is not None:`) is the readable way to chain Optionals when you need to reach through two layers — it binds the intermediate value so you don't recompute or rebind. Use it when it clarifies; use a temporary variable when it doesn't."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "**`x or default` is a footgun on non-string types.** `name or 'stranger'` is fine if `\"\"` should be treated as missing. `count or 100` silently replaces `0` with `100` — almost always a bug for integer counts. Prefer the ternary `x if x is not None else default` when you specifically mean \"`None` means use the default\".\n",
    "\n",
    "**`assert x is not None`** tells the checker and the runtime. It's fine for internal invariants, dangerous for user-facing code — an assert raises `AssertionError` at runtime if the assumption is wrong, which is a thin error for anyone debugging. Prefer `if x is None: raise ValueError(...)` for anything that could bite users.\n",
    "\n",
    "**Return empty collections, not `None`, when the caller wants to iterate.** `list[str] | None` forces every caller to `if result is not None:`. `list[str]` that can be empty lets callers loop without guarding — the for-loop over `[]` just does nothing. Reach for `Optional` when there's a meaningful difference between \"failed\" and \"succeeded with no results\"; for most collection-returning functions, there isn't.\n",
    "\n",
    "**Dict `.get(key, default)` narrows for free.** With a default, the return type collapses to the value type — no `| None`. Without one, it's `V | None` and you need to narrow. A small thing, but a nice reason to always pass the default when you have one."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Type a function signature](https://agilearn.co.uk/guides/type-hints/recipes/type-a-function-signature) — `X | None = None` and related parameter patterns.\n",
    "- [Avoid common typing mistakes](https://agilearn.co.uk/guides/type-hints/recipes/avoid-common-typing-mistakes) — the `= None` versus `| None = None` trap in detail.\n",
    "- [Truthiness rules](https://agilearn.co.uk/guides/conditional-logic/reference/truthiness-rules) — why `if x:` hits too many values.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}