# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "marimo",
# ]
# ///

import marimo

__generated_with = "0.10.19"
app = marimo.App()


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        """
        # 🛡️ Handling errors

        Sometimes things go wrong in programs. When that happens, Python raises `exceptions` to tell you what went amiss. For example, maybe you divided by 0:
        """
    )
    return


@app.cell
def _():
    1 / 0
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        """
        That's a lot of red! The outputs above are Python telling you that
        something went wrong — in this case, we tried dividing a number by 0.

        Python provides tools to catch and handle exceptions: the `try/except`
        block. This is demonstrated in the next couple cells.
        """
    )
    return


@app.cell
def _():
    # Try changing the value of divisor below, and see how the output changes.
    divisor = 0
    return (divisor,)


@app.cell
def _(divisor):
    try:
        print(1 / divisor)
    except ZeroDivisionError as e:
        print("Something went wrong!", e)
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        """
        Python has many types of Exceptions besides `ZeroDivisionError`. If you
        don't know what kind of exception you're handling, catch the generic
        `Exception` type:

        ```python
        try:
            ...
        except Exception:
            ...
        ```
        """
    )
    return


@app.cell(hide_code=True)
def _(error_types):
    error_types
    return


@app.cell(hide_code=True)
def _(mo):
    # Choose error type
    error_types = mo.ui.dropdown(
        value="ZeroDivisionError",
        options=[
            "ZeroDivisionError", 
            "TypeError", 
            "ValueError", 
            "IndexError", 
            "KeyError"
        ],
        label="Learn about ..."
    )
    return (error_types,)


@app.cell(hide_code=True)
def _(error_types, mo):
    # Error explanation
    error_explanations = {
        "ZeroDivisionError": """
        ### 🚫 ZeroDivisionError
        - Occurs when you try to divide by zero
        - Mathematical impossibility
        - Example:
        ```python
        x = 10 / 0  # Triggers ZeroDivisionError
        ```
        """,
        "TypeError": """
        ### 🔀 TypeError
        - Happens when an operation is applied to an inappropriate type
        - Mixing incompatible types
        - Example:
        ```python
        "2" + 3  # Can't add string and integer
        ```
        """,
        "ValueError": """
        ### 📊 ValueError
        - Raised when a function receives an argument of correct type 
          but inappropriate value
        - Example:
        ```python
        int("hello")  # Can't convert non-numeric string to int
        ```
        """,
        "IndexError": """
        ### 📑 IndexError
        - Occurs when trying to access a list index that doesn't exist
        - Going beyond list boundaries
        - Example:
        ```python
        my_list = [1, 2, 3]
        print(my_list[5])  # Only has indices 0, 1, 2
        ```
        """,
        "KeyError": """
        ### 🗝️ KeyError
        - Raised when trying to access a dictionary key that doesn't exist
        - Example:
        ```python
        my_dict = {"a": 1, "b": 2}
        print(my_dict["c"])  # "c" key doesn't exist
        ```
        """
    }

    mo.md(error_explanations.get(error_types.value, "Select an error type"))
    return (error_explanations,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        """
        ## Handling multiple exception types
        
        Catch and handle different types of errors specifically:

        ```python
        def complex_function(x, y):
            try:
                # Potential errors: TypeError, ZeroDivisionError
                result = x / y
                return int(result)
            except TypeError:
                return "Type mismatch!"
            except ZeroDivisionError:
                return "No division by zero!"
            except ValueError:
                return "Conversion error!"
            finally:
                # The `finally` block always runs, regardless if there
                # was an error or not
                ...
                
        ```
        """
    )
    return


@app.cell(hide_code=True)
def _(finally_input):
    finally_input
    return


@app.cell(hide_code=True)
def _(mo):
    # Finally Block Demonstration
    finally_input = mo.ui.switch(
        label="Throw an error?",
        value=True
    )
    return (finally_input,)


@app.cell
def _(finally_input, mo):
    def simulate_resource_management():
        try:
            # Simulating a resource-intensive operation
            if not finally_input.value:
                return "🟢 Resource processing successful"
            else:
                raise Exception("Simulated failure")
        except Exception as e:
            return f"🔴 Error: {e}"
        finally:
            return "📦 Resource cleanup completed"


    _result = simulate_resource_management()

    mo.md(f"""
    ### Example: the finally clause

    **Scenario**: {"Normal operation" if not finally_input.value else "An exception was raised"}

    **Result**: {_result}

    Notice how the `finally` block always runs, ensuring cleanup!
    """)
    return (simulate_resource_management,)


@app.cell
def _():
    import marimo as mo
    return (mo,)


if __name__ == "__main__":
    app.run()