{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "l12-c000",
   "metadata": {},
   "source": [
    "# Lecture 13 - Exception handling and floating-point values\n",
    "\n",
    "**Authors:** Fiona McNeill, Matteo Degiacomi, Rayo Verweij, and Nigel Topham\n",
    "\n",
    "## Introduction\n",
    "\n",
    "In the lectures so far, we have already seen many different Python errors. Here, we will discuss how you can handle errors automatically in your code and even raise your own ones, turning them from blockers into assets.\n",
    "\n",
    "### Learning outcomes\n",
    "\n",
    "In this notebook, we'll discuss:\n",
    "\n",
    "- recognising syntax errors, runtime exceptions, and logic errors;\n",
    "- handling expected failures with `try...except` blocks;\n",
    "- using `else` and `finally` to separate success and cleanup;\n",
    "- throwing manual exceptions with `raise`; and\n",
    "- explaining floating-point approximation and comparing floats with `math.isclose()`.\n",
    "\n",
    "### Using the notebook\n",
    "\n",
    "Remember, this notebook is _yours_! You can edit each cell, for example by adding new examples or changing values to see what happens. Feel free to experiment!\n",
    "\n",
    "<details>\n",
    "<summary>Click to expand</summary>\n",
    "\n",
    "A common approach for learning how to read and write code in a structured way is called **PRIMM**:\n",
    "1. **Predict**: before running the code, look at it - do you understand each line? What do you think will happen?\n",
    "2. **Run**: now run the code and see what output you get.\n",
    "3. **Investigate**: did it go as you thought it would? If not, can you figure out why?\n",
    "4. **Modify**: now change the code and once again try to predict the output. If you got it wrong the first time, did you get it right now?\n",
    "5. **Make**: finally, try to write your own program from scratch using the skills you learned.\n",
    "\n",
    "You can apply steps 1-4 to each code cell in the notebook and many will have prediction prompts to help you get started. In addition, there will be challenges that ask you to put everything together yourself and execute step 5. There will often be multiple ways to solve these, but each challenge has a potential answer listed.\n",
    "\n",
    "</details>\n",
    "\n",
    "### Navigating the notebook\n",
    "\n",
    "A Jupyter notebook can be operated with your keyboard. Here is a **cheat sheet**:\n",
    "\n",
    "<details>\n",
    "<summary>Click to expand</summary>\n",
    "\n",
    "- To run the selected cell and move to the next cell, press <kbd>&#x21E7; Shift</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To run the selected cell and keep focus there, press <kbd>⌃ Ctrl</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To create a cell under the one currently highlighted, press <kbd>B</kbd>;\n",
    "- To delete the currently highlighted cell, press <kbd>D</kbd> twice (be careful with this one!) or press <kbd>X</kbd> to cut it;\n",
    "- To get help for a specific function, place the cursor within the function's brackets, hold <kbd>&#x21E7; Shift</kbd>, and press <kbd>&#x21E5; Tab</kbd>.\n",
    "\n",
    "</details>\n",
    "\n",
    "Watch out: **code cells remember what happened in cells before**. So, especially in the more complicated notebooks later down the line, make sure to always run **every** cell from top to bottom, as one might rely on a piece of code that came before it! If your results become confusing, use **Kernel > Restart Kernel** and then rerun all the completed cells in order.\n",
    "\n",
    "### Further reading and practice\n",
    "\n",
    "- Official Python tutorial: [Errors and exceptions](https://docs.python.org/3/tutorial/errors.html) and [Floating-point arithmetic](https://docs.python.org/3/tutorial/floatingpoint.html).\n",
    "- W3Schools: [Try...Except](https://www.w3schools.com/python/python_try_except.asp).\n",
    "- Helsinki Python MOOC: [Handling errors](https://programming-26.mooc.fi/part-6/3-errors)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fdec5ea6",
   "metadata": {},
   "source": [
    "## 1. Errors, so far"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1d9e847",
   "metadata": {},
   "source": [
    "As you'll have noticed by now, there can be many different reasons why a program does not work the way you want it to. Broadly speaking, we can distinguish three types of problems:\n",
    "\n",
    "- A **syntax error** breaks the grammar of a programming language, meaning a program cannot be run until it is fixed. For example, using a single equals sign instead of a double one and vice versa.\n",
    "- A **runtime error** is an error that occurs while code is running. The program has valid syntax and is able to be run, but while running, it encounters an operation that it cannot complete, such as passing the wrong type to a function.\n",
    "- A **logic error** is an error in your algorithm design that causes you to get unexpected output even if the program compiles and runs fine. This will not cause an exception; you'll have to catch them using tests!\n",
    "\n",
    "To aid you in debugging, Python has built-in names for many of the syntax and runtime errors that can occur. Here are the ones we have seen so far:\n",
    "\n",
    "| Error | Lecture | Purpose |\n",
    "| --- | --- | --- |\n",
    "| `NameError` | Lecture 1 | Python cannot find a name that is used |\n",
    "| `TypeError` | Lecture 2 | An operation received an unsuitable type |\n",
    "| `ValueError` | Lecture 2 | The type is acceptable, but the value is unsuitable |\n",
    "| `SyntaxError` | Lecture 3 | The code breaks Python's grammar |\n",
    "| `IndentationError` | Lecture 3 | The indentation breaks Python's grammar |\n",
    "| `IndexError` | Lecture 5 | A sequence index is outside its valid range |\n",
    "| `KeyError` | Lecture 5 | A dictionary does not have the key that is used |\n",
    "| `AssertionError` | Lecture 9 | An assertion expected to be true was false |\n",
    "| `FileNotFoundError` | Lecture 10 | A requested file or directory cannot be found |\n",
    "\n",
    "In this list, `SyntaxError` and `IndentationError` are both types of syntax errors, while all others are types of runtime errors."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0cf94ae2",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: classify the problem</b>\n",
    "\n",
    "Classify each example below as a syntax error, runtime exception, or logic error; name the exception if Python would raise one; and fix each example.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d8c4aee5",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# A\n",
    "int(\"three\")\n",
    "\n",
    "# B\n",
    "average = 4 + 8 / 2\n",
    "\n",
    "# C\n",
    "score = 60\n",
    "if score > 50\n",
    "    print(\"Pass\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "439011ba",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for the solution</b></summary>\n",
    "\n",
    "```python\n",
    "# A: Runtime error (ValueError) - int() needs numeric text such as \"3\"\n",
    "int(\"3\")\n",
    "\n",
    "# B: Logic error (not reported) - group the addition before dividing\n",
    "average = (4 + 8) / 2\n",
    "\n",
    "# B: Syntax error (SyntaxError) - the header needs a colon\n",
    "score = 60\n",
    "if score > 50:\n",
    "    print(\"Pass\")\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1068dddc",
   "metadata": {},
   "source": [
    "## 2. Exception handling"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c001",
   "metadata": {},
   "source": [
    "We've seen it a few times now: when getting `input()` from a user, we add a little note that they should only type input of a certain type, for example numbers that can cleanly be converted to integers. While this is fine for short example programs, when you write code that real people use, it would be great to handle things a bit more gracefully. If the user gives the wrong type of input, wouldn't it be great to give them a helpful message rather than a crashing program?\n",
    "\n",
    "We can do this using a `try...except` block. Within the `try` block, we write an operation of which we know it might fail. Then, the `except` clause _catches_ the potential error and specifies a response.\n",
    "\n",
    "Think of `try...except` like an `if` statement, but specfically for errors. If the operation succeeds, Python skips the exception handler, but if it raises the named exception, Python leaves the `try` block immediately and runs the body of the `except` block.\n",
    "\n",
    "**Predict:** Which message will be printed? What would be printed if you changed `text` to `\"12\"`?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c002",
   "metadata": {},
   "outputs": [],
   "source": [
    "text = \"twelve\"\n",
    "\n",
    "try:\n",
    "    number = int(text)\n",
    "except ValueError:\n",
    "    print(repr(text), \"is not a whole number\")\n",
    "\n",
    "print(\"The conversion attempt has finished\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c003",
   "metadata": {},
   "source": [
    "Keep the `try` block small so it is clear which operation you expect to fail. Catching an exception should give the program a useful next step - don't use it to just hide your bugs!\n",
    "\n",
    "Note that the `except` block requires you to specifiy which exception you expect to be raised. To illustrate, let's look at a new type of exception: `ZeroDivisionError`, raised when we divide by zero.\n",
    "\n",
    "A program that converts two strings into numbers and divides their values could fail in two different ways: the strings provided could not contain numbers, leading to a `ValueError`, and the denominator could be 0, leading to a `ZeroDivisionError`. Conveniently, Python allows us to write more than one `except` clause to handle different errors in a way that's most appropriate to each."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c006",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: handle division failures</b>\n",
    "\n",
    "Finish `safe_ratio()`. Convert the arguments to floats and divide the first by the second. Catch `ValueError` and `ZeroDivisionError` separately, printing a helpful message and returning `None` in each case. Otherwise return the result.\n",
    "\n",
    "Use the test cases provided to check your work.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c007",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "def safe_ratio(numerator_text, denominator_text):\n",
    "\tpass\n",
    "\n",
    "print(safe_ratio(\"10\", \"4\"))\n",
    "print(safe_ratio(\"ten\", \"4\"))\n",
    "print(safe_ratio(\"10\", \"0\"))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c008",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "def safe_ratio(numerator_text, denominator_text):\n",
    "    try:\n",
    "        numerator = float(numerator_text)\n",
    "        denominator = float(denominator_text)\n",
    "        result = numerator / denominator\n",
    "    except ValueError:\n",
    "        print(\"Both values must be numbers\")\n",
    "        return None\n",
    "    except ZeroDivisionError:\n",
    "        print(\"The denominator cannot be zero\")\n",
    "        return None\n",
    "    return result\n",
    "\n",
    "print(safe_ratio(\"10\", \"4\"))\n",
    "print(safe_ratio(\"ten\", \"4\"))\n",
    "print(safe_ratio(\"10\", \"0\"))\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2185d26d",
   "metadata": {},
   "source": [
    "## 3. Using `else` and `finally` with exceptions"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c009",
   "metadata": {},
   "source": [
    "A `try` statement can have two more parts:\n",
    "\n",
    "- `else` runs if the `try` block completes normally, without an exception or an early exit such as `return`. Code that depends on succesfully passing `try` goes here. Note that exceptions raised inside `else` are not caught by the preceding handlers.\n",
    "- `finally` runs when execution leaves the statement, whether or not there was an exception, *including before a pending `return`!* Use it for cleanup that must happen regardless of the occurrence of an error.\n",
    "\n",
    "The usage of the keyword `else` might seem counterintuitive, unless you consider the `except` block as being a kind of `if` statement. In human language, you could read the following code like this: *Try to convert `text`. If it fails with a `ValueError`, return `None`. If not, return the converted value. Regardless of what happens, print the final message.*\n",
    "\n",
    "**Predict:** Write down the messages printed for each call below. What happens to the last line of the function?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c010",
   "metadata": {},
   "outputs": [],
   "source": [
    "def demonstrate_sections(text):\n",
    "    try:\n",
    "        value = int(text)\n",
    "    except ValueError:\n",
    "        print(\"Except: conversion failed\")\n",
    "        return None\n",
    "    else:\n",
    "        print(f\"Else: conversion produced {value}\")\n",
    "        return value\n",
    "    finally:\n",
    "        print(\"Finally: attempt complete\")\n",
    "    print(\"After: is this ever printed?\")\n",
    "\n",
    "demonstrate_sections(\"25\")\n",
    "print(\"---\")\n",
    "demonstrate_sections(\"twenty-five\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c011",
   "metadata": {},
   "source": [
    "The `finally` message appears for both calls, but the last line never does: both the `except` and `else` branches `return`, so execution never reaches the line after the `try` statement. This is what `finally` adds - something to remember, as it can be quite unexpected behaviour. Without the early returns, an ordinary line after the `try` statement would run in both cases too.\n",
    "\n",
    "Avoid `return` inside `finally`, because it can replace a result or suppress an exception that was about to leave the function."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c012",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: separate success from handling</b>\n",
    "\n",
    "Write `percentage_from_text(text)`, which tries converting a string containing a proportion, such as `\"0.25\"`, to a float. If a `ValueError` occurs, print a message and return `None`. If not, return the proportion as a percentage. In either case, print *Conversion attempt finished*.\n",
    "\n",
    "Try both `\"0.25\"` and `\"quarter\"` as inputs. When is the final message printed relative to the returned result?\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c013",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your code here!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c014",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "The `finally` block runs before the caller receives and prints the return value.\n",
    "\n",
    "```python\n",
    "def percentage_from_text(text):\n",
    "    try:\n",
    "        proportion = float(text)\n",
    "    except ValueError:\n",
    "        print(\"Enter a numeric proportion\")\n",
    "        return None\n",
    "    else:\n",
    "        return f\"{proportion * 100}%\"\n",
    "    finally:\n",
    "        print(\"Conversion attempt finished\")\n",
    "\n",
    "print(percentage_from_text(\"0.25\"))\n",
    "print(percentage_from_text(\"quarter\"))\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6b3968b2",
   "metadata": {},
   "source": [
    "## 4. Handling file errors"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c015",
   "metadata": {},
   "source": [
    "Let's practice catching errors a bit more by thinking through what might come up when working with files. For starters, a requested file might not exist. This is not always a problem: for example, apps and games often save a user's settings in local files. The program would then check whether a settings file exists, and use it if so, or create a new file if not.\n",
    "\n",
    "As discussed, `FileNotFoundError` tells us that a requested path cannot be found.\n",
    "\n",
    "**Predict:** What happens if the following example file is absent? What happens if you run the program a second time?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c016",
   "metadata": {},
   "outputs": [],
   "source": [
    "settings = \"settings.txt\"\n",
    "\n",
    "try:\n",
    "    with open(settings, \"r\", encoding=\"utf-8\") as file:\n",
    "        text = file.read()\n",
    "except FileNotFoundError:\n",
    "    default = 3\n",
    "    with open(settings, \"w\", encoding=\"utf-8\") as file:\n",
    "        file.write(str(default))\n",
    "else:\n",
    "    print(\"Read:\", repr(text))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c017",
   "metadata": {},
   "source": [
    "Next, a file can exist but contain unsuitable data. This is a different problem from a missing file, so once again we use separate file handlers. The following function expects a settings file like the one we created above, containing just a single integer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c018",
   "metadata": {},
   "outputs": [],
   "source": [
    "def read_one_integer(path):\n",
    "    try:\n",
    "        with open(path, \"r\", encoding=\"utf-8\") as file:\n",
    "            text = file.read()\n",
    "        return int(text)\n",
    "    except FileNotFoundError:\n",
    "        print(f\"File not found: {path}\")\n",
    "    except ValueError:\n",
    "        print(f\"File did not contain one integer: {path}\")\n",
    "    return None\n",
    "\n",
    "print(read_one_integer(settings))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c019",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: read an optional setting</b>\n",
    "\n",
    "Write `read_scale(path)` to read a float from a text file. If the file is missing or its contents are not numeric, return `1.0` and print a different message for each case.\n",
    "\n",
    "Create one example file containing the text `2.5` and another containing `large`, and check both. Try the missing path too.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c020",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your code here!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c021",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "def read_scale(path):\n",
    "    try:\n",
    "        with open(path, \"r\", encoding=\"utf-8\") as file:\n",
    "            return float(file.read().strip())\n",
    "    except FileNotFoundError:\n",
    "        print(\"No scale file; using the default\")\n",
    "        return 1.0\n",
    "    except ValueError:\n",
    "        print(\"Invalid scale; using the default\")\n",
    "        return 1.0\n",
    "\n",
    "with open(\"ip26_l13_scale.txt\", \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"2.5\")\n",
    "with open(\"ip26_l13_bad_scale.txt\", \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"large\")\n",
    "\n",
    "assert read_scale(\"ip26_l13_scale.txt\") == 2.5\n",
    "assert read_scale(\"ip26_l13_bad_scale.txt\") == 1.0\n",
    "\n",
    "print(read_scale(\"ip26_l13_no_scale.txt\"))\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3424d1ed",
   "metadata": {},
   "source": [
    "## 5. Raising an exception"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c022",
   "metadata": {},
   "source": [
    "Raising exceptions is not just reserved for Python... we can do it ourselves too! If we want our own functions to throw errors, we can do that using the `raise` keyword. Pass it the name of the error and an informative message.\n",
    "\n",
    "In `except` blocks, we can then access this message using the `as` keyword which saves it to a variable. You can call it anything, but here we'll simply call it `error`.\n",
    "\n",
    "**Predict:** Which call below returns a number? Where does the other call stop?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c023",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mean(numbers):\n",
    "    \"\"\"Return the arithmetic mean of a non-empty list of numbers\"\"\"\n",
    "    if len(numbers) == 0:\n",
    "        raise ValueError(\"Calculating the mean requires at least one number\")\n",
    "    return sum(numbers) / len(numbers)\n",
    "\n",
    "print(mean([2, 4, 6]))\n",
    "\n",
    "try:\n",
    "    mean([])\n",
    "except ValueError as error:\n",
    "    print(f\"Could not calculate mean: {error}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c024",
   "metadata": {},
   "source": [
    "The calculation function raises the exception and the caller decides what to display. If no caller handles an exception, the program crashes, it reaches the notebook, and is shown as a traceback. This movement back through callers is called **propagation**.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: reject an invalid percentage</b>\n",
    "\n",
    "Define `checked_percentage(value)` for a numeric argument. Return the value if it is from 0 to 100 inclusive, and raise `ValueError` otherwise.\n",
    "\n",
    "Try both boundary values, a value below zero, and a value above 100. Handle the invalid cases in the calling code.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c026",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your code here!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c027",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "def checked_percentage(value):\n",
    "    \"\"\"Return a numeric percentage if it is between 0 and 100.\"\"\"\n",
    "    if value < 0 or value > 100:\n",
    "        raise ValueError(\"percentage must be from 0 to 100\")\n",
    "    return value\n",
    "\n",
    "for value in [0, 100, -1, 110]:\n",
    "    try:\n",
    "        print(checked_percentage(value))\n",
    "    except ValueError as error:\n",
    "        print(error)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c958b6c8",
   "metadata": {},
   "source": [
    "## 6. Floating-point approximation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c028",
   "metadata": {},
   "source": [
    "That's it for `try...except` blocks, but before we end the lecture, we should discuss one final peculiarity of many programming language that often leads to errors.\n",
    "\n",
    "**Predict:** What will the following code print?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c029",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = 0.1 + 0.2\n",
    "print(result)\n",
    "print(result == 0.3)\n",
    "print(f\"{result:.20f}\")\t\t# Format with 20 decimal numbers"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c030",
   "metadata": {},
   "source": [
    "Wait, what?\n",
    "\n",
    "Let's think about how computers store numbers. Computers use a binary rather than decimal system: everything is either a 1 or 0, so 2 becomes 10, 3 is 11, 4 is 100, 5 is 101, et cetera. This works fine for integers, but how would you ever store a fraction? While some, like 0.25, are finite, many of them like 1/3 or π have an infinite number of digits. As a result, computers have to store fractional values an *approximation* of themselves.\n",
    "\n",
    "This is why Python separates integers and floats as separate types, as alluded to in Lecture 2. Integers can store their number *precisely*, but floats are stored as fractions of binary numbers, which are often slightly inaccurate. Note that what counts as a finite fraction is different when using binary than when using base ten, which is why 0.1 is finite for us but not for a computer.\n",
    "\n",
    "As a result, calculations using floats are often slightly off. While the difference is often tiny, it is often still noticeable, for example when comparing numbers such as in the program above.\n",
    "\n",
    "To deal with this, the `math` library has a function `isclose()` that we can use to check whether a calculated float is approximately equal to an expected result. It returns a Boolean, using a tolerance rather than requiring exact equality."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c031",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.isclose(0.1 + 0.2, 0.3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ad24496",
   "metadata": {},
   "source": [
    "The function allows us to set the required level of preciseness using optional parameters.\n",
    "\n",
    "- `rel_tol` sets *relative tolerance*, an allowed difference as a fraction of the larger magnitude. For example, for values around 1000, `rel_tol=0.001` allows a difference of about 1.\n",
    "- `abs_tol` sets *absolute tolerance*, an allowed difference independent of magnitude. It is especially useful when comparing with zero.\n",
    "\n",
    "By default, `rel_tol` is `0.000000001` and `abs_tol` is `0.0`. Choose tolerances according to what counts as an acceptable difference in your problem.\n",
    "\n",
    "**Predict:** Which of the following comparisons are true, and which false?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0237f640",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.isclose(0.0, 0.000001))\n",
    "print(math.isclose(1000.0, 1000.1, rel_tol=0.001))\n",
    "print(math.isclose(0.0, 0.000001, abs_tol=0.00001))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c032",
   "metadata": {},
   "source": [
    "With only a relative tolerance, a result that should be zero but isn't exactly zero never counts as close. For example, `math.isclose(0.1 + 0.2 - 0.3, 0.0)` returns `False`, because `0.1 + 0.2 - 0.3` gives a tiny non-zero number. When comparing with zero, give an `abs_tol` too.\n",
    "\n",
    "When you want *only* an absolute tolerance, set `rel_tol=0.0` explicitly."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c033",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: test a float result</b>\n",
    "\n",
    "Define `celsius_to_fahrenheit(celsius)` using `celsius * 9 / 5 + 32`.\n",
    "\n",
    "Calculate the result for 38.1°C and use an assertion with `math.isclose()` to check it against 100.58°F. Use a relative tolerance of `0.0` and an absolute tolerance of `0.000000001`.\n",
    "\n",
    "What happens if you use `==` instead?\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-c034",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "import math\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-c035",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "import math\n",
    "\n",
    "def celsius_to_fahrenheit(celsius):\n",
    "    return celsius * 9 / 5 + 32\n",
    "\n",
    "result = celsius_to_fahrenheit(38.1)\n",
    "print(result)\n",
    "print(result == 100.58)  # False: the result is 100.58000000000001\n",
    "assert math.isclose(result, 100.58, rel_tol=0.0, abs_tol=0.000000001)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-consolidation-prompt",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "## Consolidation challenge\n",
    "\n",
    "Let's finish with one more challenge to bring everything together.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: load temperatures safely</b>\n",
    "\n",
    "Write `load_temperatures(path)` to read one float per non-empty line of a file and return a list of those values. Raise `ValueError` with a line number if a line is not numeric and also if there are no readings, so a caller will not accidentally divide by zero when calculating a mean. Separately, handle what happens if the file is not found.\n",
    "\n",
    "Create a file containing `12`, a blank line, and `13`. Load it and check that the mean is close to `12.5`. Then try a file containing invalid text and an empty file.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l12-consolidation-work",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your code here!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-consolidation-solution",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "import math\n",
    "\n",
    "def load_temperatures(path):\n",
    "    values = []\n",
    "    with open(path, \"r\", encoding=\"utf-8\") as file:\n",
    "        for line_number, line in enumerate(file, start=1):\n",
    "            text = line.strip()\n",
    "            if text == \"\":\n",
    "                continue\n",
    "            try:\n",
    "                value = float(text)\n",
    "            except ValueError:\n",
    "                raise ValueError(f\"invalid temperature on line {line_number}\")\n",
    "            values.append(value)\n",
    "    if len(values) == 0:\n",
    "        raise ValueError(\"temperature file contains no readings\")\n",
    "    return values\n",
    "\n",
    "examples = {\n",
    "    \"ip26_l13_temperatures.txt\": \"12\\n\\n13\\n\",\n",
    "    \"ip26_l13_bad_temperatures.txt\": \"12\\nwarm\\n\",\n",
    "    \"ip26_l13_empty_temperatures.txt\": \"\",\n",
    "    \"ip26_l13_missing_temperatures.txt\": None,  # Never written, so it stays missing\n",
    "}\n",
    "for path, contents in examples.items():\n",
    "    if contents is not None:\n",
    "        with open(path, \"w\", encoding=\"utf-8\") as file:\n",
    "            file.write(contents)\n",
    "    try:\n",
    "        readings = load_temperatures(path)\n",
    "    except FileNotFoundError:\n",
    "        print(f\"Temperature file is missing: {path}\")\n",
    "    except ValueError as error:\n",
    "        print(error)\n",
    "    else:\n",
    "        average = sum(readings) / len(readings)\n",
    "        assert math.isclose(average, 12.5, rel_tol=0.0, abs_tol=0.000000001)\n",
    "        print(\"Mean:\", average)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l12-consolidation-summary",
   "metadata": {
    "tags": [
     "summary"
    ]
   },
   "source": [
    "## Summary\n",
    "\n",
    "Well done, your programs have just become a *lot* more robust. Note that in this lecture we used a bunch of terms interchangeably: *error* and *exception*, *throwing* and *raising*, *catching* and *handling*. While different programming languages might prefer one over the other, in human language, it does not matter which in each pair you use.\n",
    "\n",
    "\n",
    "### Key terms\n",
    "\n",
    "<dl>\n",
    "\t<dt><b>Syntax error</b></dt>\n",
    "\t<dd>Code that breaks Python's grammar and cannot be run as written.</dd>\n",
    "\t<dt><b>Runtime exception</b></dt>\n",
    "\t<dd>A problem reported while an operation is running.</dd>\n",
    "\t<dt><b>Logic error</b></dt>\n",
    "\t<dd>Code that runs but does not produce the intended behaviour.</dd>\n",
    "\t<dt><b>Exception handler</b></dt>\n",
    "\t<dd>An <code>except</code> block that responds to a matching exception.</dd>\n",
    "\t<dt><b>Propagation</b></dt>\n",
    "\t<dd>An exception moving back through callers until a matching handler catches it or it crashes the program.</dd>\n",
    "\t<dt><b>Cleanup</b></dt>\n",
    "\t<dd>Work done when an operation finishes, even if the operation failed.</dd>\n",
    "\t<dt><b>Floating-point approximation</b></dt>\n",
    "\t<dd>The small difference caused by representing a number with a finite binary float.</dd>\n",
    "\t<dt><b>Tolerance</b></dt>\n",
    "\t<dd>An acceptable difference when comparing approximate numbers, either relative to magnitude or an absolute amount.</dd>\n",
    "</dl>\n",
    "\n",
    "### New syntax\n",
    "\n",
    "<dl>\n",
    "\t<dt><code>try:</code></dt>\n",
    "\t<dd>Begins a block containing an operation that may raise an exception.</dd>\n",
    "\t<dt><code>except XError:</code></dt>\n",
    "\t<dd>Handles a matching exception raised by the <code>try</code> block.</dd>\n",
    "\t<dt><code>except XError as error:</code></dt>\n",
    "\t<dd>Also gives the caught exception a name so its message can be inspected.</dd>\n",
    "\t<dt><code>else:</code> (after <code>except</code>)</dt>\n",
    "\t<dd>Runs a block when <code>try</code> completes normally without an exception or early exit.</dd>\n",
    "\t<dt><code>finally:</code></dt>\n",
    "\t<dd>Runs a block as execution leaves the statement, whether or not an exception occurred. When used inside a function, runs <em>before</em> any <code>return</code> statements inside the earlier <code>try...except</code> branches.</dd>\n",
    "\t<dt><code>raise XError(message)</code></dt>\n",
    "\t<dd>Raises an exception with an explaining message.</dd>\n",
    "\t<dt><code>ZeroDivisionError</code></dt>\n",
    "\t<dd>An exception raised by division by zero.</dd>\n",
    "\t<dt><code>math.isclose(a, b, rel_tol=..., abs_tol=...)</code></dt>\n",
    "\t<dd>Compares numbers within relative or absolute tolerances, returning a Boolean.</dd>\n",
    "</dl>\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
