{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exception types\n",
    "\n",
    "In this tutorial, you will explore the built-in exception hierarchy in Python. You will learn how Python organises exceptions into a class hierarchy, how to handle specific exception types, and how to use the `Exception` base class.\n",
    "\n",
    "**Time commitment:** 15–20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- Completion of [Your first exception](https://agilearn.co.uk/guides/error-handling/learn/01-your-first-exception)\n",
    "- Basic understanding of `try`/`except` blocks\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- List the most common built-in exception types and when they occur\n",
    "- Explain the exception class hierarchy in Python\n",
    "- Handle multiple exception types in a single `try`/`except` block\n",
    "- Use the `Exception` base class to handle broad categories of exceptions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Common built-in exceptions\n",
    "\n",
    "Python provides many built-in exception types, each representing a specific kind of error. Here are the ones you will encounter most often:\n",
    "\n",
    "| Exception | When it is raised |\n",
    "|-----------|-------------------|\n",
    "| `ValueError` | A function receives a value of the correct type but an inappropriate value |\n",
    "| `TypeError` | An operation is applied to an object of an inappropriate type |\n",
    "| `KeyError` | A dictionary key is not found |\n",
    "| `IndexError` | A sequence index is out of range |\n",
    "| `FileNotFoundError` | A file or directory is requested but does not exist |\n",
    "| `ZeroDivisionError` | Division or modulo by zero |\n",
    "| `AttributeError` | An attribute reference or assignment fails |\n",
    "| `NameError` | A local or global name is not found |\n",
    "\n",
    "Let us see each of these in action."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ValueError: inappropriate value for the type\n",
    "try:\n",
    "    number = int(\"hello\")\n",
    "except ValueError as e:\n",
    "    print(f\"ValueError: {e}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TypeError: inappropriate type for the operation\n",
    "try:\n",
    "    result = \"10\" + 5\n",
    "except TypeError as e:\n",
    "    print(f\"TypeError: {e}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# KeyError: key not found in dictionary\n",
    "try:\n",
    "    data = {\"name\": \"Alice\", \"age\": 30}\n",
    "    email = data[\"email\"]\n",
    "except KeyError as e:\n",
    "    print(f\"KeyError: {e}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# IndexError: list index out of range\n",
    "try:\n",
    "    numbers = [1, 2, 3]\n",
    "    value = numbers[10]\n",
    "except IndexError as e:\n",
    "    print(f\"IndexError: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The exception hierarchy\n",
    "\n",
    "All built-in exceptions in Python are organised into a class hierarchy. At the very top is `BaseException`, and most exceptions you will handle inherit from `Exception`.\n",
    "\n",
    "Here is a simplified view of the hierarchy:\n",
    "\n",
    "```\n",
    "BaseException\n",
    "├── SystemExit\n",
    "├── KeyboardInterrupt\n",
    "├── GeneratorExit\n",
    "└── Exception\n",
    "    ├── ArithmeticError\n",
    "    │   ├── ZeroDivisionError\n",
    "    │   ├── OverflowError\n",
    "    │   └── FloatingPointError\n",
    "    ├── LookupError\n",
    "    │   ├── IndexError\n",
    "    │   └── KeyError\n",
    "    ├── OSError\n",
    "    │   ├── FileNotFoundError\n",
    "    │   ├── PermissionError\n",
    "    │   └── IsADirectoryError\n",
    "    ├── ValueError\n",
    "    ├── TypeError\n",
    "    ├── AttributeError\n",
    "    └── NameError\n",
    "```\n",
    "\n",
    "This hierarchy matters because when you handle a parent exception, you also handle all its children."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling parent exceptions\n",
    "\n",
    "Because `ZeroDivisionError` inherits from `ArithmeticError`, you can handle it using either type."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Handling the parent class catches the child exception\n",
    "try:\n",
    "    result = 10 / 0\n",
    "except ArithmeticError as e:\n",
    "    print(f\"Caught an ArithmeticError: {e}\")\n",
    "    print(f\"Actual type: {type(e).__name__}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Similarly, `IndexError` and `KeyError` both inherit from `LookupError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LookupError handles both IndexError and KeyError\n",
    "def safe_lookup(collection, key):\n",
    "    \"\"\"Safely look up a value in a list or dictionary.\"\"\"\n",
    "    try:\n",
    "        return collection[key]\n",
    "    except LookupError as e:\n",
    "        print(f\"Lookup failed ({type(e).__name__}): {e}\")\n",
    "        return None\n",
    "\n",
    "\n",
    "# Works with lists (IndexError)\n",
    "safe_lookup([1, 2, 3], 10)\n",
    "\n",
    "# Works with dictionaries (KeyError)\n",
    "safe_lookup({\"a\": 1}, \"z\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling multiple exception types\n",
    "\n",
    "You can handle different exception types in separate `except` clauses. Python checks each clause in order and runs the first one that matches."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def convert_and_divide(value: str, divisor: float) -> float | None:\n",
    "    \"\"\"Convert a string to a number and divide it by the divisor.\"\"\"\n",
    "    try:\n",
    "        number = float(value)\n",
    "        return number / divisor\n",
    "    except ValueError:\n",
    "        print(f\"Cannot convert {value!r} to a number.\")\n",
    "        return None\n",
    "    except ZeroDivisionError:\n",
    "        print(\"Cannot divide by zero.\")\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(convert_and_divide(\"10\", 3))     # Normal operation\n",
    "print(convert_and_divide(\"hello\", 3))   # Triggers ValueError\n",
    "print(convert_and_divide(\"10\", 0))      # Triggers ZeroDivisionError"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling multiple exceptions in one clause\n",
    "\n",
    "If you want to handle several exception types in the same way, you can list them in a tuple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def parse_number(value: str) -> float | None:\n",
    "    \"\"\"Parse a string to a number, handling both ValueError and TypeError.\"\"\"\n",
    "    try:\n",
    "        return float(value)\n",
    "    except (ValueError, TypeError) as e:\n",
    "        print(f\"Could not parse {value!r}: {e}\")\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(parse_number(\"3.14\"))    # Valid float string\n",
    "print(parse_number(\"abc\"))     # Triggers ValueError\n",
    "print(parse_number(None))      # Triggers TypeError"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Checking the exception hierarchy with `issubclass()`\n",
    "\n",
    "You can use `issubclass()` to verify the relationships between exception types."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ZeroDivisionError is a subclass of ArithmeticError\n",
    "print(issubclass(ZeroDivisionError, ArithmeticError))\n",
    "\n",
    "# ArithmeticError is a subclass of Exception\n",
    "print(issubclass(ArithmeticError, Exception))\n",
    "\n",
    "# FileNotFoundError is a subclass of OSError\n",
    "print(issubclass(FileNotFoundError, OSError))\n",
    "\n",
    "# KeyError is a subclass of LookupError\n",
    "print(issubclass(KeyError, LookupError))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why you should handle specific exceptions\n",
    "\n",
    "It can be tempting to handle `Exception` (or worse, use a bare `except`) to deal with all possible errors at once. However, this is generally a bad practice because it can hide bugs in your code.\n",
    "\n",
    "Consider the following example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Bad practice: catching all exceptions hides bugs\n",
    "def bad_divide(a: float, b: float) -> float | None:\n",
    "    \"\"\"A poorly written divide function that hides bugs.\"\"\"\n",
    "    try:\n",
    "        return a / b\n",
    "    except Exception:\n",
    "        # This hides ALL errors, including ones you did not expect\n",
    "        return None\n",
    "\n",
    "\n",
    "# This hides a TypeError that indicates a bug in your code\n",
    "print(bad_divide(\"ten\", 2))  # Returns None instead of revealing the bug"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Good practice: handle only the exceptions you expect\n",
    "def good_divide(a: float, b: float) -> float | None:\n",
    "    \"\"\"A well-written divide function that handles only expected exceptions.\"\"\"\n",
    "    try:\n",
    "        return a / b\n",
    "    except ZeroDivisionError:\n",
    "        return None\n",
    "\n",
    "\n",
    "# This correctly reveals the bug as a TypeError\n",
    "try:\n",
    "    good_divide(\"ten\", 2)\n",
    "except TypeError as e:\n",
    "    print(f\"Bug revealed: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By handling only `ZeroDivisionError`, the `TypeError` propagates up and reveals the bug. This makes debugging much easier."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "### Exercise 1: Identify the exception\n",
    "\n",
    "Write a function called `identify_exception` that takes a callable (a function) and calls it inside a `try`/`except` block. It should handle `ValueError`, `TypeError`, `KeyError`, and `IndexError`, and return a string naming which exception was raised. If no exception occurs, return `\"no exception\"`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections.abc import Callable\n",
    "\n",
    "\n",
    "def identify_exception(func: Callable) -> str:\n",
    "    \"\"\"Call the function and return the name of any exception raised.\"\"\"\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 collections.abc import Callable\n",
    "\n",
    "\n",
    "def identify_exception(func: Callable) -> str:\n",
    "    \"\"\"Call the function and return the name of any exception raised.\"\"\"\n",
    "    try:\n",
    "        func()\n",
    "        return \"no exception\"\n",
    "    except ValueError:\n",
    "        return \"ValueError\"\n",
    "    except TypeError:\n",
    "        return \"TypeError\"\n",
    "    except KeyError:\n",
    "        return \"KeyError\"\n",
    "    except IndexError:\n",
    "        return \"IndexError\"\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercise 2: Using parent exceptions\n",
    "\n",
    "Write a function called `safe_access` that takes a collection (list or dictionary) and a key, and returns the value. Use `LookupError` to handle both `IndexError` and `KeyError` in a single `except` clause. Return `None` if the lookup fails."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "\n",
    "\n",
    "def safe_access(collection: list | dict, key: Any) -> Any:\n",
    "    \"\"\"Safely access a value from a list or dictionary.\"\"\"\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 safe_access(collection: list | dict, key: Any) -> Any:\n",
    "    \"\"\"Safely access a value from a list or dictionary.\"\"\"\n",
    "    try:\n",
    "        return collection[key]\n",
    "    except LookupError:\n",
    "        return None\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this tutorial, you learned the following:\n",
    "\n",
    "- Python provides many **built-in exception types**, each representing a specific kind of error\n",
    "- Exceptions are organised in a **class hierarchy**, with `BaseException` at the top and `Exception` as the parent of most exceptions you will handle\n",
    "- Handling a **parent exception** also handles all its child exceptions\n",
    "- You can handle **multiple exception types** using separate `except` clauses or by grouping them in a tuple\n",
    "- Always handle **specific exceptions** rather than using broad `except` clauses, to avoid hiding bugs\n",
    "\n",
    "In the next tutorial, [Raising exceptions](https://agilearn.co.uk/guides/error-handling/learn/03-raising-exceptions), you will learn how to raise your own exceptions and create custom exception classes."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}