{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Your first exception\n",
    "\n",
    "In this tutorial, you will learn the fundamentals of exception handling in Python. You will write your first `try`/`except` block and learn how to handle common exceptions like `ZeroDivisionError` and `FileNotFoundError`.\n",
    "\n",
    "**Time commitment:** 15–20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- Basic Python knowledge (variables, functions, and control flow)\n",
    "- A Python 3.12 or later installation\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Explain what an exception is and why exceptions occur\n",
    "- Use `try`/`except` blocks to handle exceptions\n",
    "- Handle specific exception types such as `ZeroDivisionError` and `ValueError`\n",
    "- Print informative messages when exceptions occur"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What is an exception?\n",
    "\n",
    "An **exception** is an event that occurs during program execution and disrupts the normal flow of instructions. When Python encounters a situation it cannot handle, it **raises** an exception.\n",
    "\n",
    "Let us see what happens when Python encounters an error."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This will raise a ZeroDivisionError\n",
    "# Uncomment the line below to see the exception:\n",
    "# result = 10 / 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you uncomment and run the line above, Python will display a **traceback** — a detailed report showing where the error occurred and what type of exception was raised.\n",
    "\n",
    "The traceback ends with a line like:\n",
    "\n",
    "```\n",
    "ZeroDivisionError: division by zero\n",
    "```\n",
    "\n",
    "This tells you two things:\n",
    "\n",
    "1. The **type** of exception: `ZeroDivisionError`\n",
    "2. A **message** describing the problem: `division by zero`\n",
    "\n",
    "Without exception handling, this error would cause your program to stop immediately. Let us learn how to handle it gracefully."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `try`/`except` block\n",
    "\n",
    "The `try`/`except` block is the foundation of exception handling in Python. It allows you to attempt an operation and specify what should happen if an exception occurs.\n",
    "\n",
    "The basic structure is as follows:\n",
    "\n",
    "```python\n",
    "try:\n",
    "    # Code that might raise an exception\n",
    "except ExceptionType:\n",
    "    # Code that runs if the exception occurs\n",
    "```\n",
    "\n",
    "Let us try it with a division operation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    result = 10 / 0\n",
    "except ZeroDivisionError:\n",
    "    print(\"Cannot divide by zero.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Excellent! Instead of crashing, the program printed a helpful message. Here is what happened:\n",
    "\n",
    "1. Python attempted to execute `10 / 0` inside the `try` block\n",
    "2. A `ZeroDivisionError` was raised\n",
    "3. Python jumped to the `except` block and ran the code there\n",
    "4. The program continued normally after the `try`/`except` block"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling exceptions in functions\n",
    "\n",
    "Exception handling is especially useful inside functions, where you want to deal with potential errors and return a meaningful result. Let us write a function that safely divides two numbers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def safe_divide(a: float, b: float) -> float | None:\n",
    "    \"\"\"Safely divide two numbers, returning None if division by zero occurs.\"\"\"\n",
    "    try:\n",
    "        return a / b\n",
    "    except ZeroDivisionError:\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(safe_divide(10, 2))   # Normal division\n",
    "print(safe_divide(10, 0))   # Division by zero\n",
    "print(safe_divide(-6, 3))   # Negative numbers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function returns `None` when division by zero occurs, rather than crashing. The caller can then check for `None` and respond appropriately."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling `ValueError`\n",
    "\n",
    "A `ValueError` is raised when a function receives a value of the correct type but an inappropriate value. A common example is converting a non-numeric string to an integer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def parse_integer(value: str) -> int | None:\n",
    "    \"\"\"Convert a string to an integer, returning None if conversion fails.\"\"\"\n",
    "    try:\n",
    "        return int(value)\n",
    "    except ValueError:\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(parse_integer(\"42\"))      # Valid integer string\n",
    "print(parse_integer(\"hello\"))   # Not a number\n",
    "print(parse_integer(\"3.14\"))    # Float string (not a valid integer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that `\"3.14\"` also returns `None` because `int()` does not accept strings containing decimal points."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling `FileNotFoundError`\n",
    "\n",
    "When working with files, a `FileNotFoundError` is raised if you try to open a file that does not exist. This is one of the most common exceptions you will encounter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def read_file_safely(filepath: str) -> str | None:\n",
    "    \"\"\"Read the contents of a file, returning None if the file is not found.\"\"\"\n",
    "    try:\n",
    "        with open(filepath, \"r\", encoding=\"utf-8\") as f:\n",
    "            return f.read()\n",
    "    except FileNotFoundError:\n",
    "        print(f\"File not found: {filepath}\")\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This file does not exist, so the exception handler will run\n",
    "content = read_file_safely(\"nonexistent_file.txt\")\n",
    "print(f\"Result: {content}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function prints a helpful message and returns `None` instead of letting the program crash."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Accessing the exception object\n",
    "\n",
    "You can access the exception object using the `as` keyword. This gives you access to the error message and other details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    number = int(\"not_a_number\")\n",
    "except ValueError as e:\n",
    "    print(f\"An exception occurred: {e}\")\n",
    "    print(f\"Exception type: {type(e).__name__}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The variable `e` holds the exception object. You can convert it to a string to get the error message, or inspect its type using `type()`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What happens without exception handling?\n",
    "\n",
    "To appreciate why exception handling matters, consider what happens when an exception is not handled. The program stops immediately, and any code after the error never runs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def process_numbers(numbers: list[str]) -> list[int]:\n",
    "    \"\"\"Convert a list of strings to integers, skipping invalid values.\"\"\"\n",
    "    results = []\n",
    "    for item in numbers:\n",
    "        try:\n",
    "            results.append(int(item))\n",
    "        except ValueError:\n",
    "            print(f\"Skipping invalid value: {item!r}\")\n",
    "    return results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = [\"10\", \"20\", \"abc\", \"30\", \"xyz\", \"40\"]\n",
    "valid_numbers = process_numbers(data)\n",
    "print(f\"Valid numbers: {valid_numbers}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Without the `try`/`except` block, the function would have crashed on `\"abc\"` and never processed `\"30\"`, `\"xyz\"`, or `\"40\"`. Exception handling allows the function to skip invalid values and continue processing."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "Try these exercises to reinforce what you have learned.\n",
    "\n",
    "### Exercise 1: Safe square root\n",
    "\n",
    "Write a function called `safe_square_root` that takes a number and returns its square root. If the number is negative, handle the `ValueError` and return `None`.\n",
    "\n",
    "**Hint:** Use `math.sqrt()` from the `math` module."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "# Write your solution here\n",
    "def safe_square_root(number: float) -> float | None:\n",
    "    \"\"\"Return the square root of a number, or None if the number is negative.\"\"\"\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",
    "import math\n",
    "\n",
    "\n",
    "def safe_square_root(number: float) -> float | None:\n",
    "    \"\"\"Return the square root of a number, or None if the number is negative.\"\"\"\n",
    "    try:\n",
    "        return math.sqrt(number)\n",
    "    except ValueError:\n",
    "        return None\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercise 2: Safe dictionary access\n",
    "\n",
    "Write a function called `get_value` that takes a dictionary and a key. It should return the value for that key. If the key does not exist, handle the `KeyError` and return a default value of `\"unknown\"`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "\n",
    "# Write your solution here\n",
    "def get_value(data: dict, key: str) -> Any:\n",
    "    \"\"\"Get a value from a dictionary, returning 'unknown' if the key is missing.\"\"\"\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",
    "from typing import Any\n",
    "\n",
    "\n",
    "def get_value(data: dict, key: str) -> Any:\n",
    "    \"\"\"Get a value from a dictionary, returning 'unknown' if the key is missing.\"\"\"\n",
    "    try:\n",
    "        return data[key]\n",
    "    except KeyError:\n",
    "        return \"unknown\"\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this tutorial, you learned the following:\n",
    "\n",
    "- An **exception** is an event that disrupts the normal flow of a program\n",
    "- The `try`/`except` block lets you handle exceptions gracefully\n",
    "- You should handle **specific** exception types such as `ZeroDivisionError`, `ValueError`, and `FileNotFoundError`\n",
    "- The `as` keyword lets you access the exception object for more details\n",
    "- Exception handling allows your program to continue running when errors occur\n",
    "\n",
    "In the next tutorial, [Exception types](https://agilearn.co.uk/guides/error-handling/learn/02-exception-types), you will explore the built-in exception hierarchy and learn how Python organises its exceptions into a class hierarchy."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}