{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Raising exceptions\n",
    "\n",
    "In this tutorial, you will learn how to use the `raise` statement to signal errors in your own code. You will also create custom exception classes that make your code easier to understand and debug.\n",
    "\n",
    "**Time commitment:** 15–20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- Completion of [Exception types](https://agilearn.co.uk/guides/error-handling/learn/02-exception-types)\n",
    "- Understanding of `try`/`except` blocks and built-in exception types\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Use the `raise` statement to signal errors in your functions\n",
    "- Choose the appropriate built-in exception type to raise\n",
    "- Create custom exception classes by inheriting from `Exception`\n",
    "- Add attributes and messages to custom exception classes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why raise exceptions?\n",
    "\n",
    "So far, you have learned how to handle exceptions that Python raises automatically. But what about errors that are specific to your own code?\n",
    "\n",
    "For example, suppose you write a function that calculates a person's age. Negative ages do not make sense, so your function should signal an error when it receives one. This is where the `raise` statement comes in."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `raise` statement\n",
    "\n",
    "The `raise` statement lets you signal that an exceptional condition has occurred. You provide an exception object, and Python interrupts the normal flow of execution.\n",
    "\n",
    "```python\n",
    "raise ExceptionType(\"A message describing the error\")\n",
    "```\n",
    "\n",
    "Let us see it in practice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def validate_age(age: int) -> int:\n",
    "    \"\"\"Validate that the given age is a non-negative integer.\n",
    "\n",
    "    Args:\n",
    "        age: The age value to validate.\n",
    "\n",
    "    Returns:\n",
    "        The validated age.\n",
    "\n",
    "    Raises:\n",
    "        ValueError: If the age is negative or greater than 150.\n",
    "        TypeError: If the age is not an integer.\n",
    "    \"\"\"\n",
    "    if not isinstance(age, int):\n",
    "        raise TypeError(f\"Age must be an integer, got {type(age).__name__}\")\n",
    "    if age < 0:\n",
    "        raise ValueError(f\"Age must be non-negative, got {age}\")\n",
    "    if age > 150:\n",
    "        raise ValueError(f\"Age must be 150 or less, got {age}\")\n",
    "    return age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Valid age\n",
    "print(validate_age(25))\n",
    "\n",
    "# Invalid age: the exception is raised and can be handled\n",
    "try:\n",
    "    validate_age(-5)\n",
    "except ValueError as e:\n",
    "    print(f\"Validation failed: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice how the error message clearly describes what went wrong. Good error messages make debugging much easier."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Choosing the right exception type\n",
    "\n",
    "When raising exceptions, choose the built-in exception type that best describes the error:\n",
    "\n",
    "| Use this exception | When |\n",
    "|--------------------|------|\n",
    "| `ValueError` | The value is the right type but not acceptable |\n",
    "| `TypeError` | The argument is the wrong type |\n",
    "| `FileNotFoundError` | A required file does not exist |\n",
    "| `PermissionError` | The program lacks permission for an operation |\n",
    "| `RuntimeError` | An error that does not fit other categories |\n",
    "| `NotImplementedError` | A method or feature is not yet implemented |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_average(numbers: list[float]) -> float:\n",
    "    \"\"\"Calculate the average of a list of numbers.\n",
    "\n",
    "    Args:\n",
    "        numbers: A list of numbers.\n",
    "\n",
    "    Returns:\n",
    "        The average value.\n",
    "\n",
    "    Raises:\n",
    "        TypeError: If the argument is not a list.\n",
    "        ValueError: If the list is empty.\n",
    "    \"\"\"\n",
    "    if not isinstance(numbers, list):\n",
    "        raise TypeError(f\"Expected a list, got {type(numbers).__name__}\")\n",
    "    if len(numbers) == 0:\n",
    "        raise ValueError(\"Cannot calculate the average of an empty list\")\n",
    "    return sum(numbers) / len(numbers)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Normal usage\n",
    "print(calculate_average([10, 20, 30]))\n",
    "\n",
    "# Empty list\n",
    "try:\n",
    "    calculate_average([])\n",
    "except ValueError as e:\n",
    "    print(f\"ValueError: {e}\")\n",
    "\n",
    "# Wrong type\n",
    "try:\n",
    "    calculate_average(\"not a list\")\n",
    "except TypeError as e:\n",
    "    print(f\"TypeError: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Creating custom exception classes\n",
    "\n",
    "Sometimes, built-in exception types are not specific enough for your needs. In these cases, you can create your own exception classes by inheriting from `Exception`.\n",
    "\n",
    "A custom exception class is a regular Python class with one requirement: it must inherit from `Exception` (or one of its subclasses)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class InsufficientFundsError(Exception):\n",
    "    \"\"\"Raised when a withdrawal exceeds the available balance.\"\"\"\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def withdraw(balance: float, amount: float) -> float:\n",
    "    \"\"\"Withdraw an amount from the balance.\n",
    "\n",
    "    Args:\n",
    "        balance: The current account balance.\n",
    "        amount: The amount to withdraw.\n",
    "\n",
    "    Returns:\n",
    "        The new balance after the withdrawal.\n",
    "\n",
    "    Raises:\n",
    "        InsufficientFundsError: If the amount exceeds the balance.\n",
    "        ValueError: If the amount is negative.\n",
    "    \"\"\"\n",
    "    if amount < 0:\n",
    "        raise ValueError(\"Withdrawal amount must be non-negative\")\n",
    "    if amount > balance:\n",
    "        raise InsufficientFundsError(\n",
    "            f\"Cannot withdraw {amount}: only {balance} available\"\n",
    "        )\n",
    "    return balance - amount"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Normal withdrawal\n",
    "print(withdraw(100.0, 30.0))\n",
    "\n",
    "# Insufficient funds\n",
    "try:\n",
    "    withdraw(50.0, 75.0)\n",
    "except InsufficientFundsError as e:\n",
    "    print(f\"Transaction declined: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Adding attributes to custom exceptions\n",
    "\n",
    "Custom exception classes can store extra information by defining an `__init__` method. This is useful when the exception handler needs details about what went wrong."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ValidationError(Exception):\n",
    "    \"\"\"Raised when a validation check fails.\n",
    "\n",
    "    Attributes:\n",
    "        field: The name of the field that failed validation.\n",
    "        reason: A description of the validation failure.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, field: str, reason: str) -> None:\n",
    "        self.field = field\n",
    "        self.reason = reason\n",
    "        super().__init__(f\"Validation failed for '{field}': {reason}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def validate_email(email: str) -> str:\n",
    "    \"\"\"Validate that a string looks like an email address.\n",
    "\n",
    "    Args:\n",
    "        email: The email address to validate.\n",
    "\n",
    "    Returns:\n",
    "        The validated email address.\n",
    "\n",
    "    Raises:\n",
    "        ValidationError: If the email address is not valid.\n",
    "    \"\"\"\n",
    "    if \"@\" not in email:\n",
    "        raise ValidationError(\"email\", \"must contain '@'\")\n",
    "    if \".\" not in email.split(\"@\")[1]:\n",
    "        raise ValidationError(\"email\", \"domain must contain '.'\")\n",
    "    return email"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Valid email\n",
    "print(validate_email(\"alice@example.com\"))\n",
    "\n",
    "# Invalid email: access the exception attributes\n",
    "try:\n",
    "    validate_email(\"not-an-email\")\n",
    "except ValidationError as e:\n",
    "    print(f\"Error: {e}\")\n",
    "    print(f\"Field: {e.field}\")\n",
    "    print(f\"Reason: {e.reason}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The exception handler can access `e.field` and `e.reason` to understand exactly what went wrong, without having to parse the error message string."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Re-raising exceptions\n",
    "\n",
    "Sometimes you want to handle an exception partially (for example, to log it) and then let it propagate to the caller. You can do this with a bare `raise` statement inside an `except` block."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def process_data(data: list[str]) -> list[int]:\n",
    "    \"\"\"Convert a list of strings to integers.\n",
    "\n",
    "    Args:\n",
    "        data: A list of numeric strings.\n",
    "\n",
    "    Returns:\n",
    "        A list of integers.\n",
    "\n",
    "    Raises:\n",
    "        ValueError: If any string cannot be converted to an integer.\n",
    "    \"\"\"\n",
    "    results = []\n",
    "    for item in data:\n",
    "        try:\n",
    "            results.append(int(item))\n",
    "        except ValueError:\n",
    "            print(f\"Warning: could not convert {item!r}\")\n",
    "            raise  # Re-raise the exception to the caller\n",
    "    return results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    process_data([\"1\", \"2\", \"abc\"])\n",
    "except ValueError as e:\n",
    "    print(f\"Processing failed: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function logged a warning and then re-raised the same exception so the caller could decide how to handle it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "### Exercise 1: Temperature validator\n",
    "\n",
    "Write a function called `validate_temperature` that takes a temperature in Celsius (a float) and raises a `ValueError` if the temperature is below absolute zero (−273.15 °C). If valid, return the temperature."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def validate_temperature(celsius: float) -> float:\n",
    "    \"\"\"Validate that a temperature is above absolute 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 validate_temperature(celsius: float) -> float:\n",
    "    \"\"\"Validate that a temperature is above absolute zero.\"\"\"\n",
    "    if celsius < -273.15:\n",
    "        raise ValueError(\n",
    "            f\"Temperature cannot be below absolute zero (-273.15), got {celsius}\"\n",
    "        )\n",
    "    return celsius\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercise 2: Custom exception\n",
    "\n",
    "Create a custom exception class called `InvalidPasswordError` with attributes `password_length` (an integer) and `reason` (a string). Then write a function called `validate_password` that raises `InvalidPasswordError` if the password is shorter than eight characters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class InvalidPasswordError(Exception):\n",
    "    \"\"\"Raised when a password does not meet the requirements.\"\"\"\n",
    "    pass  # Replace this with your implementation\n",
    "\n",
    "\n",
    "def validate_password(password: str) -> str:\n",
    "    \"\"\"Validate that a password meets minimum length requirements.\"\"\"\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",
    "class InvalidPasswordError(Exception):\n",
    "    \"\"\"Raised when a password does not meet the requirements.\"\"\"\n",
    "\n",
    "    def __init__(self, password_length: int, reason: str) -> None:\n",
    "        self.password_length = password_length\n",
    "        self.reason = reason\n",
    "        super().__init__(f\"Invalid password (length {password_length}): {reason}\")\n",
    "\n",
    "\n",
    "def validate_password(password: str) -> str:\n",
    "    \"\"\"Validate that a password meets minimum length requirements.\"\"\"\n",
    "    if len(password) < 8:\n",
    "        raise InvalidPasswordError(\n",
    "            password_length=len(password),\n",
    "            reason=\"must be at least 8 characters\",\n",
    "        )\n",
    "    return password\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this tutorial, you learned the following:\n",
    "\n",
    "- The `raise` statement lets you signal errors in your own code\n",
    "- Choose the **appropriate built-in exception type** for the error (for example, `ValueError` for bad values, `TypeError` for wrong types)\n",
    "- You can create **custom exception classes** by inheriting from `Exception`\n",
    "- Custom exceptions can carry **extra attributes** that help the exception handler respond to the error\n",
    "- A bare `raise` inside an `except` block **re-raises** the current exception\n",
    "\n",
    "In the next tutorial, [Cleanup with finally](https://agilearn.co.uk/guides/error-handling/learn/04-cleanup-with-finally), you will learn how to use the `finally` and `else` clauses, and how context managers help with resource cleanup."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}