{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cleanup with finally\n",
    "\n",
    "In this tutorial, you will learn how to use the `finally` and `else` clauses in `try`/`except` blocks, and how to use context managers with the `with` statement for reliable resource cleanup.\n",
    "\n",
    "**Time commitment:** 15–20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- Completion of [Raising exceptions](https://agilearn.co.uk/guides/error-handling/learn/03-raising-exceptions)\n",
    "- Understanding of `try`/`except` blocks, exception types, and the `raise` statement\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Use the `finally` clause to guarantee cleanup code runs\n",
    "- Use the `else` clause to run code only when no exception occurs\n",
    "- Write the full `try`/`except`/`else`/`finally` structure\n",
    "- Use context managers with the `with` statement for resource management"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The problem: unreliable cleanup\n",
    "\n",
    "When your code opens a file, establishes a network connection, or acquires any other resource, you need to release that resource when you are finished. But what if an exception occurs before the cleanup code runs?\n",
    "\n",
    "Consider this example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tempfile\n",
    "import os\n",
    "\n",
    "# Create a temporary file for demonstration\n",
    "temp_path = os.path.join(tempfile.gettempdir(), \"demo_data.txt\")\n",
    "with open(temp_path, \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(\"10\\n20\\nabc\\n40\\n\")\n",
    "\n",
    "print(f\"Created temporary file: {temp_path}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Without finally, an exception could leave the file open\n",
    "f = open(temp_path, \"r\", encoding=\"utf-8\")\n",
    "try:\n",
    "    content = f.read()\n",
    "    print(f\"Read {len(content)} characters\")\n",
    "except FileNotFoundError:\n",
    "    print(\"File not found\")\n",
    "# If an unexpected exception occurs above, the file stays open\n",
    "f.close()\n",
    "print(\"File closed successfully\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The code above has a flaw: if an unexpected exception occurs between `open()` and `f.close()`, the file will never be closed. The `finally` clause solves this problem."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `finally` clause\n",
    "\n",
    "The `finally` clause contains code that **always** runs, whether an exception occurred or not. This makes it ideal for cleanup operations.\n",
    "\n",
    "```python\n",
    "try:\n",
    "    # Code that might raise an exception\n",
    "except SomeException:\n",
    "    # Handle the exception\n",
    "finally:\n",
    "    # This always runs, no matter what\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# With finally, the file is always closed\n",
    "f = open(temp_path, \"r\", encoding=\"utf-8\")\n",
    "try:\n",
    "    content = f.read()\n",
    "    print(f\"Read {len(content)} characters\")\n",
    "except FileNotFoundError:\n",
    "    print(\"File not found\")\n",
    "finally:\n",
    "    f.close()\n",
    "    print(\"File closed (guaranteed by finally)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `finally` block runs regardless of whether an exception was raised, whether it was handled, or whether the code completed normally."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `finally` runs even when exceptions are unhandled\n",
    "\n",
    "An important property of `finally` is that it runs even when the exception is **not** handled by any `except` clause. Let us demonstrate this with a safe example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demonstrate_finally(value: int) -> str:\n",
    "    \"\"\"Demonstrate that finally always runs.\"\"\"\n",
    "    resource = \"acquired\"\n",
    "    try:\n",
    "        if value == 0:\n",
    "            raise ValueError(\"Zero is not allowed\")\n",
    "        return f\"Success: {100 / value}\"\n",
    "    except ValueError as e:\n",
    "        return f\"Handled: {e}\"\n",
    "    finally:\n",
    "        resource = \"released\"\n",
    "        print(f\"Finally block executed. Resource: {resource}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Normal execution: finally still runs\n",
    "print(demonstrate_finally(5))\n",
    "print()\n",
    "\n",
    "# Handled exception: finally still runs\n",
    "print(demonstrate_finally(0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the `finally` block executed in both cases, even when a `return` statement was reached in the `try` or `except` block."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `else` clause\n",
    "\n",
    "The `else` clause runs only when the `try` block completes **without** raising an exception. This is useful for code that should run only on success.\n",
    "\n",
    "```python\n",
    "try:\n",
    "    # Code that might raise an exception\n",
    "except SomeException:\n",
    "    # Handle the exception\n",
    "else:\n",
    "    # Runs only if no exception occurred\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def safe_divide(a: float, b: float) -> float | None:\n",
    "    \"\"\"Divide two numbers, demonstrating the else clause.\"\"\"\n",
    "    try:\n",
    "        result = a / b\n",
    "    except ZeroDivisionError:\n",
    "        print(\"Division by zero is not allowed.\")\n",
    "        return None\n",
    "    else:\n",
    "        print(f\"Division successful: {a} / {b} = {result}\")\n",
    "        return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Success: else clause runs\n",
    "safe_divide(10, 3)\n",
    "print()\n",
    "\n",
    "# Failure: else clause does not run\n",
    "safe_divide(10, 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using `else` is better than putting the success code inside `try`, because it prevents accidentally handling exceptions raised by the success code itself."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The full structure: `try`/`except`/`else`/`finally`\n",
    "\n",
    "You can combine all four clauses for complete control over exception handling.\n",
    "\n",
    "```python\n",
    "try:\n",
    "    # Attempt the operation\n",
    "except SomeException:\n",
    "    # Handle the exception\n",
    "else:\n",
    "    # Runs only if no exception occurred\n",
    "finally:\n",
    "    # Always runs (cleanup)\n",
    "```\n",
    "\n",
    "The clauses must appear in this order: `try`, then `except`, then `else`, then `finally`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def read_numbers_from_file(filepath: str) -> list[int]:\n",
    "    \"\"\"Read integers from a file, one per line.\n",
    "\n",
    "    Args:\n",
    "        filepath: Path to the file containing numbers.\n",
    "\n",
    "    Returns:\n",
    "        A list of integers read from the file.\n",
    "    \"\"\"\n",
    "    numbers = []\n",
    "    f = None\n",
    "    try:\n",
    "        f = open(filepath, \"r\", encoding=\"utf-8\")\n",
    "        for line in f:\n",
    "            numbers.append(int(line.strip()))\n",
    "    except FileNotFoundError:\n",
    "        print(f\"File not found: {filepath}\")\n",
    "    except ValueError as e:\n",
    "        print(f\"Invalid data in file: {e}\")\n",
    "    else:\n",
    "        print(f\"Successfully read {len(numbers)} numbers\")\n",
    "    finally:\n",
    "        if f is not None:\n",
    "            f.close()\n",
    "            print(\"File closed\")\n",
    "    return numbers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create a valid file\n",
    "valid_path = os.path.join(tempfile.gettempdir(), \"valid_numbers.txt\")\n",
    "with open(valid_path, \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(\"10\\n20\\n30\\n\")\n",
    "\n",
    "print(\"--- Reading valid file ---\")\n",
    "result = read_numbers_from_file(valid_path)\n",
    "print(f\"Numbers: {result}\")\n",
    "print()\n",
    "\n",
    "print(\"--- Reading nonexistent file ---\")\n",
    "result = read_numbers_from_file(\"/nonexistent/path.txt\")\n",
    "print(f\"Numbers: {result}\")\n",
    "print()\n",
    "\n",
    "print(\"--- Reading file with invalid data ---\")\n",
    "result = read_numbers_from_file(temp_path)\n",
    "print(f\"Numbers: {result}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Context managers: the `with` statement\n",
    "\n",
    "Writing `try`/`finally` blocks for every resource can be tedious and error-prone. Python provides a cleaner alternative: the **context manager** using the `with` statement.\n",
    "\n",
    "A context manager automatically handles setup and cleanup. The most common example is opening files."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Without context manager (verbose and error-prone)\n",
    "f = open(valid_path, \"r\", encoding=\"utf-8\")\n",
    "try:\n",
    "    content = f.read()\n",
    "finally:\n",
    "    f.close()\n",
    "\n",
    "print(f\"Without context manager: read {len(content)} characters\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# With context manager (clean and safe)\n",
    "with open(valid_path, \"r\", encoding=\"utf-8\") as f:\n",
    "    content = f.read()\n",
    "\n",
    "print(f\"With context manager: read {len(content)} characters\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Both examples do the same thing, but the `with` statement is shorter, clearer, and guarantees the file will be closed even if an exception occurs.\n",
    "\n",
    "When the `with` block ends (whether normally or because of an exception), Python automatically calls the cleanup method on the context manager."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Combining `with` and `try`/`except`\n",
    "\n",
    "You can combine context managers with `try`/`except` blocks for both resource management and exception handling."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_lines(filepath: str) -> int:\n",
    "    \"\"\"Count the number of lines in a file.\n",
    "\n",
    "    Args:\n",
    "        filepath: Path to the file.\n",
    "\n",
    "    Returns:\n",
    "        The number of lines, or -1 if the file cannot be read.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        with open(filepath, \"r\", encoding=\"utf-8\") as f:\n",
    "            return sum(1 for _ in f)\n",
    "    except FileNotFoundError:\n",
    "        print(f\"File not found: {filepath}\")\n",
    "        return -1\n",
    "    except PermissionError:\n",
    "        print(f\"Permission denied: {filepath}\")\n",
    "        return -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Lines in valid file: {count_lines(valid_path)}\")\n",
    "print(f\"Lines in missing file: {count_lines('/nonexistent/file.txt')}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `with` statement handles closing the file, whilst the `try`/`except` block handles the exceptions. This is the recommended pattern for working with files and other resources."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "### Exercise 1: Read and sum\n",
    "\n",
    "Write a function called `read_and_sum` that reads numbers from a file (one per line) and returns their sum. Use a context manager for the file, handle `FileNotFoundError` and `ValueError`, and return `0` if an error occurs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def read_and_sum(filepath: str) -> float:\n",
    "    \"\"\"Read numbers from a file and return their sum.\"\"\"\n",
    "    pass  # Replace this with your implementation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>Click to reveal the solution</summary>\n",
    "\n",
    "```python\n",
    "def read_and_sum(filepath: str) -> float:\n",
    "    \"\"\"Read numbers from a file and return their sum.\"\"\"\n",
    "    try:\n",
    "        with open(filepath, \"r\", encoding=\"utf-8\") as f:\n",
    "            total = sum(float(line.strip()) for line in f)\n",
    "    except FileNotFoundError:\n",
    "        print(f\"File not found: {filepath}\")\n",
    "        return 0\n",
    "    except ValueError as e:\n",
    "        print(f\"Invalid data: {e}\")\n",
    "        return 0\n",
    "    else:\n",
    "        return total\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercise 2: Try/except/else/finally\n",
    "\n",
    "Write a function called `safe_reciprocal` that takes a number and returns its reciprocal (1 divided by the number). Use the full `try`/`except`/`else`/`finally` structure:\n",
    "\n",
    "- `try`: Perform the division\n",
    "- `except`: Handle `ZeroDivisionError` and return `None`\n",
    "- `else`: Print the successful result\n",
    "- `finally`: Print `\"Calculation complete\"`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def safe_reciprocal(number: float) -> float | None:\n",
    "    \"\"\"Return the reciprocal of a number, or None if the number is zero.\"\"\"\n",
    "    pass  # Replace this with your implementation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>Click to reveal the solution</summary>\n",
    "\n",
    "```python\n",
    "def safe_reciprocal(number: float) -> float | None:\n",
    "    \"\"\"Return the reciprocal of a number, or None if the number is zero.\"\"\"\n",
    "    try:\n",
    "        result = 1 / number\n",
    "    except ZeroDivisionError:\n",
    "        print(\"Cannot compute the reciprocal of zero.\")\n",
    "        return None\n",
    "    else:\n",
    "        print(f\"Reciprocal of {number} is {result}\")\n",
    "        return result\n",
    "    finally:\n",
    "        print(\"Calculation complete\")\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cleanup\n",
    "\n",
    "Let us remove the temporary files we created during this tutorial."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Remove temporary files\n",
    "for path in [temp_path, valid_path]:\n",
    "    try:\n",
    "        os.remove(path)\n",
    "        print(f\"Removed: {path}\")\n",
    "    except FileNotFoundError:\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this tutorial, you learned the following:\n",
    "\n",
    "- The `finally` clause **always runs**, making it ideal for cleanup code\n",
    "- The `else` clause runs only when **no exception** occurred in the `try` block\n",
    "- The full structure is `try`/`except`/`else`/`finally`, and the clauses must appear in this order\n",
    "- **Context managers** (the `with` statement) provide a cleaner alternative to `try`/`finally` for resource management\n",
    "- Combine context managers with `try`/`except` for both resource management and exception handling\n",
    "\n",
    "Congratulations! You have completed the tutorials section. You now have a solid foundation in exception handling with Python. To continue learning, explore the [Recipes](https://agilearn.co.uk/guides/error-handling/recipes/index) for practical solutions to specific problems, or dive into the [Reference](https://agilearn.co.uk/guides/error-handling/reference/index) for detailed technical documentation."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}