{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Use context managers for reliable cleanup\n",
    "\n",
    "**The question.** You're acquiring a resource — a file, a database connection, a lock, a temporary directory — and you need to guarantee it gets released when you're done, even if an exception is raised in between. Writing a `try`/`finally` for each one is verbose and easy to forget.\n",
    "\n",
    "The answer: use a context manager with the `with` statement. The canonical one is `open()`; the `@contextmanager` decorator lets you build your own in a few lines whenever the built-in one doesn't fit.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The canonical pattern: open() is a context manager.\n",
    "# The 'with' block guarantees f.close() runs — on success, on exception, on early return.\n",
    "from pathlib import Path\n",
    "\n",
    "path = Path('/tmp/ctx-demo.txt')\n",
    "path.write_text('First line\\nSecond line\\nThird line\\n')\n",
    "\n",
    "# Cleanup-guaranteed form:\n",
    "with open(path, encoding='utf-8') as f:\n",
    "    content = f.read()\n",
    "\n",
    "# f is now closed — no matter how we left the block.\n",
    "print(f'read {len(content)} chars')\n",
    "print(f'file closed: {f.closed}')\n",
    "\n",
    "# Multiple resources in one 'with' (Python 3.10+):\n",
    "dest = Path('/tmp/ctx-demo-copy.txt')\n",
    "with (\n",
    "    open(path, encoding='utf-8') as src,\n",
    "    open(dest, 'w', encoding='utf-8') as dst,\n",
    "):\n",
    "    dst.write(src.read().upper())\n",
    "\n",
    "print(dest.read_text())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: write one with `@contextmanager`\n",
    "\n",
    "For simple setup/cleanup, a generator is the shortest path. `yield` splits the function into 'before' (setup) and 'after' (cleanup). Wrap the `yield` in `try`/`finally` so cleanup runs even if the body raises.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "from contextlib import contextmanager\n",
    "\n",
    "@contextmanager\n",
    "def timer(label='operation'):\n",
    "    '''Measure and print the wall-clock time of the with-block.'''\n",
    "    start = time.perf_counter()\n",
    "    try:\n",
    "        yield\n",
    "    finally:\n",
    "        elapsed = time.perf_counter() - start\n",
    "        print(f'[{label}] {elapsed:.4f}s')\n",
    "\n",
    "\n",
    "with timer('list comp'):\n",
    "    squares = [x * x for x in range(100_000)]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: a class for stateful context managers\n",
    "\n",
    "When you want the manager's object to survive the block — so you can inspect its state afterwards, say — write a class. `__enter__` returns `self`; `__exit__` sets attributes the caller reads later.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "class Timer:\n",
    "    def __init__(self, label: str = 'operation') -> None:\n",
    "        self.label = label\n",
    "        self.elapsed: float = 0.0\n",
    "\n",
    "    def __enter__(self) -> 'Timer':\n",
    "        self._start = time.perf_counter()\n",
    "        return self\n",
    "\n",
    "    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:\n",
    "        self.elapsed = time.perf_counter() - self._start\n",
    "        print(f'[{self.label}] {self.elapsed:.4f}s')\n",
    "        return False    # don't suppress exceptions\n",
    "\n",
    "\n",
    "with Timer('sum') as t:\n",
    "    total = sum(range(1_000_000))\n",
    "\n",
    "print(f'elapsed was {t.elapsed:.4f}s; total = {total}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variant: suppress expected exceptions with `contextlib.suppress`\n",
    "\n",
    "When 'the resource isn't there and that's fine', `contextlib.suppress(ExceptionType)` is clearer than a bare `except: pass`. Use for the narrow cases only — not as a general swallow.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from contextlib import suppress\n",
    "from pathlib import Path\n",
    "\n",
    "# Delete if present, ignore if not — idempotent cleanup.\n",
    "with suppress(FileNotFoundError):\n",
    "    Path('/tmp/does-not-exist-and-that-is-fine.txt').unlink()\n",
    "\n",
    "print('continued without error')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why this works\n",
    "\n",
    "A context manager is any object with `__enter__` and `__exit__` methods. `with` calls `__enter__`, runs the block, and calls `__exit__` on the way out — whether the block finishes normally or raises. `__exit__` gets the exception info if one occurred, and can suppress it by returning `True` (though you rarely should).\n",
    "\n",
    "The shape is equivalent to `try`/`finally`, but with the cleanup code written next to the acquisition code instead of wherever the exit happens. That's why the pattern scales to multiple resources: each manager's `__exit__` runs in reverse order, so resources are released in the inverse order they were acquired — the standard LIFO discipline for nested resources.\n",
    "\n",
    "When built-in managers don't cover your case, the `@contextmanager` decorator lets you write one as a generator: setup before `yield`, cleanup in a `finally` after. It's the lightweight path; a full class is the heavier but more flexible option.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "Reach for `@contextmanager` (see extra cells) when the setup/cleanup is linear and stateless — a timer, a temp directory, a database transaction. Reach for a class when you need to store state between `__enter__` and `__exit__` (a `Timer` whose `.elapsed` you'll inspect after), or when the context-manager behaviour is part of an object's broader lifecycle.\n",
    "\n",
    "`__exit__` returning `True` *suppresses* the exception. Do this sparingly — it's the silent-swallow anti-pattern in disguise. `contextlib.suppress(FileNotFoundError)` is a clean built-in for the one common case: 'try this, ignore if the resource isn't there'.\n",
    "\n",
    "Multiple context managers in one `with` statement compose cleanly (Python 3.10+ supports the parenthesised form across lines). If any `__enter__` raises, the ones that already entered are exited in reverse order — there's no leak window.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Avoid common error handling mistakes](https://agilearn.co.uk/guides/error-handling/recipes/avoid-common-mistakes) — the 'forget to close' anti-pattern.\n",
    "- [Handle multiple exceptions](https://agilearn.co.uk/guides/error-handling/recipes/handle-multiple-exceptions) — what `__exit__` sees, and why returning `True` is a tool to use sparingly.\n",
    "- `contextlib` — `suppress`, `closing`, `ExitStack`, and friends in the standard library.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}