{
"cells": [
{
"cell_type": "markdown",
"id": "d6504bb5",
"metadata": {},
"source": [
"# Exception Handling\n",
"\n",
"Today, we will explore exceptions in Python. However, before we delve into exceptions, let's briefly discuss the data structure known as the stack.\n",
"\n",
"## Stack\n",
"\n",
"A stack is a data structure that follows the Last In, First Out (LIFO) principle, meaning that the last element added to the stack is the first one to be removed. It can be visualized as a collection of elements with two main operations: push, which adds an element to the top of the stack, and pop, which removes the top element from the stack.\n",
"\n",
"Key points of stack:\n",
"\n",
"* Abstract data type\n",
"* Collection of elements\n",
"* **Push** operation: adds an element to the collection\n",
"* **Pop** operation: removes the most recently added element\n",
"\n",
"````{note}\n",
"Think of a stack data structure as a can of Pringles chips - a perfect analogy to understand how it works.\n",
"\n",
"```{toggle}\n",
"In this analogy, the can symbolizes the stack, and the chips nestled inside represent the elements stored in the stack. Here's a breakdown of how the analogy mirrors the operations of a stack:\n",
"\n",
"* **Push Operation (Adding Chips):**\n",
" * Just like adding new chips to a can, the \"push\" operation allows us to add elements to the stack, but always at the top.\n",
"* **Pop Operation (Taking the Top Chip):**\n",
" * The \"pop\" operation corresponds to taking a chip from the top of the can. It follows the principle of retrieving the most recently added element.\n",
" * If you want a chip, you reach for the top one - the last chip added to the can.\n",
"\n",
"```\n",
"\n",
"By visualizing the stack as a can of Pringles, it's easy to grasp the Last In, First Out (LIFO) nature of the stack data structure. The can's opening serves as a reminder that we can only add or take chips from the top, illustrating the strict order in which elements are managed. This analogy provides a tangible way to understand the fundamental operations of a stack.\n",
"````\n",
"\n",
"### Representing stack functionality using the `list` data type\n",
"\n",
"\n",
"We can simulate a stack data structure efficiently by leveraging the built-in list data type in Python.\n",
"\n",
"```{warning}\n",
"In reality, a list and a stack are distinct data structures. To delve deeper into their individual characteristics and functionalities, you can explore the following links:\n",
"\n",
"* Stack\n",
"* List\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4f62a277-3fe1-4f18-9854-db0fa32cd7e7",
"metadata": {
"tags": [
"hide-output"
]
},
"outputs": [],
"source": [
"stack = []\n",
"\n",
"stack.append(\"element 1\") # append ~ pop\n",
"print(f\"Stack is: {stack}\")\n",
"\n",
"stack.append(\"element 2\")\n",
"stack.append(\"element 3\")\n",
"\n",
"popped_elem = stack.pop()\n",
"print(f\"Popped element: {popped_elem}\")\n",
"print(f\"Stack is: {stack}\")"
]
},
{
"cell_type": "markdown",
"id": "cdc4115c",
"metadata": {},
"source": [
"### Call Stack\n",
"\n",
"\n",
"A call stack, often referred to simply as the \"stack,\" is a region of memory used in computer science to manage function and method calls in a program. It keeps track of the active subroutines (functions or methods) and their respective local variables. The call stack operates on the Last In, First Out (LIFO) principle, meaning that the last function call made is the first one to be resolved.\n",
"\n",
"#### Simple function calls"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "561d204a",
"metadata": {},
"outputs": [],
"source": [
"def func_a():\n",
" print(\"Function A start\")\n",
" func_b()\n",
" print(\"Function A end\")\n",
"\n",
"def func_b():\n",
" print(\"Function B start\")\n",
" print(\"Function B end\")\n",
"\n",
"# Main program\n",
"func_a()"
]
},
{
"cell_type": "markdown",
"id": "a7a36585",
"metadata": {},
"source": [
"In this example, `func_a` calls `func_b`, and the call stack looks like this:\n",
"\n",
"1. `func_a` is called.\n",
"2. Inside `func_a`, `func_b` is called.\n",
"3. `func_b` completes its execution.\n",
"4. Control returns to the point immediately after the call to `func_b` inside `func_a`.\n",
"5. `func_a` completes its execution.\n",
"\n",
"We can use the `traceback` package to display the call stack at a particular point in our code:\n",
"\n",
"```python\n",
"import traceback\n",
"\n",
"def func_a():\n",
" func_b()\n",
"\n",
"def func_b():\n",
" traceback.print_stack()\n",
"\n",
"\n",
"# Main program\n",
"func_a()\n",
"```\n",
"The output for the provided code snippet is as follows:\n",
"\n",
"```\n",
" File \"...\", line 11, in \n",
" func_a()\n",
" File \"...\", line 4, in func_a\n",
" func_b()\n",
" File \"...\", line 7, in func_b\n",
" traceback.print_stack()\n",
"```\n",
"\n",
"\n",
"#### Recursive function"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "578c56c9",
"metadata": {},
"outputs": [],
"source": [
"def factorial(n):\n",
" if n == 0 or n == 1:\n",
" return 1\n",
" else:\n",
" return n * factorial(n - 1)\n",
"\n",
"result = factorial(5)\n",
"print(\"Factorial of 5:\", result)"
]
},
{
"cell_type": "markdown",
"id": "31edf450",
"metadata": {},
"source": [
"In this example, the `factorial` function calls itself recursively.\n",
"The call stack looks like this:\n",
"```{toggle} \n",
"1. `factorial(5)` calls `factorial(4)`\n",
"2. `factorial(4)` calls `factorial(3)`\n",
"3. `factorial(3)` calls `factorial(2)`\n",
"4. `factorial(2)` calls `factorial(1)`\n",
"5. `factorial(1)` returns 1 (base case)\n",
"6. Control returns to `factorial(2)`, which returns 2 * 1 = 2\n",
"7. Control returns to `factorial(3)`, which returns 3 * 2 = 6\n",
"8. Control returns to `factorial(4)`, which returns 4 * 6 = 24\n",
"9. Control returns to `factorial(5)`, which returns 5 * 24 = 120\n",
"```\n",
"\n",
"#### Stack Overflow\n",
"\n",
"The call stack has a specific size, meaning that if a program has an excessive number of function calls, it can lead to a stack overflow error. This occurs when the available memory for the call stack is exhausted. Let's examine an example to illustrate this:\n",
"\n",
"```python\n",
"def factorial(arg):\n",
" \"\"\"Custom function calculating factorial\"\"\"\n",
" assert_msg = \"Ordinary factorial defined for\"\\\n",
" \" numbers greater or equal than zero\"\n",
" assert arg >= 0, assert_msg\n",
" if arg == 0:\n",
" return 1\n",
" return arg * factorial(arg - 1)\n",
"\n",
"print(factorial(2000))\n",
"```\n",
"\n",
"This situation emphasizes the importance of carefully managing recursion and function calls to prevent stack overflow errors, as exceeding the stack size can lead to program termination. Recursive functions should be designed with appropriate base cases to ensure that the recursion stops and doesn't lead to an infinite loop, ultimately causing a stack overflow.\n",
"\n",
"However, In Python sometimes we could need to manually increase the call stack size. In this case we can use the `sys` module:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d8d5f09a",
"metadata": {
"tags": [
"hide-output"
]
},
"outputs": [],
"source": [
"import sys\n",
"import math\n",
"\n",
"print(sys.getrecursionlimit())\n",
"sys.setrecursionlimit(10000)\n",
"\n",
"def factorial(arg):\n",
" \"\"\"Custom function calculating factorial\"\"\"\n",
" assert_msg = \"Ordinary factorial defined for\"\\\n",
" \" numbers greater or equal than zero\"\n",
" assert arg >= 0, assert_msg\n",
" if arg == 0:\n",
" return 1\n",
" return arg * factorial(arg - 1)\n",
"\n",
"print(f\"{math.log10(factorial(2000))}\")"
]
},
{
"cell_type": "markdown",
"id": "ee4f89b7",
"metadata": {},
"source": [
"```{note}\n",
"Chances are you've come across a valuable resource known as Stack Overflow. If you're curious about why it's named the way it is, here you can find a few intriguing possibilities.\n",
"```\n",
"\n",
"\n",
"Understanding the call stack becomes particularly crucial when handling exceptions and interpreting their output. A solid grasp of the call stack allows developers to navigate through the sequence of function calls leading to an exception, aiding in the identification and resolution of issues within the code.\n",
"\n",
"\n",
"## Syntax Errors and Exceptions\n",
"\n",
"In Python, errors can be broadly classified into two main categories: Syntax Errors and Exceptions. \n",
"While they both signal that something has gone awry in our code, they play different roles in the debugging and error-handling process.\n",
"\n",
"### Syntax Errors\n",
"\n",
"Let's start by looking at Syntax Errors. These occur when the Python interpreter encounters code that violates the language's syntax rules. In other words, it's like a grammatical mistake in our code.\n",
"\n",
"```python\n",
"# Example of a Syntax Error\n",
"print(\"Hello, world!\"\n",
"```\n",
"\n",
"In this example, a missing closing parenthesis will result in a SyntaxError. These errors are caught by the interpreter during the compilation phase, preventing the program from running.\n",
"\n",
"```{note}\n",
"Understanding and fixing syntax errors is fundamental. They are often the first roadblock we face when translating our ideas into executable code. Fortunately, Python provides clear error messages that guide us to the problematic lines.\n",
"```\n",
"\n",
"### Exceptions\n",
"\n",
"In Python, an exception is a runtime error that interrupts the normal flow of a program. These errors can occur due to various reasons, such as invalid input, file not found, or division by zero. Understanding and handling exceptions is crucial for writing robust and reliable Python code.\n",
"\n",
"\n",
"Python comes with a set of built-in exceptions, each serving a specific purpose. Some common built-in exceptions include:\n",
"\n",
"1. `SyntaxError`: Raised when the Python interpreter encounters a syntax error.\n",
"2. `TypeError`: Raised when an operation or function is applied to an object of an inappropriate type.\n",
"3. `ValueError`: Raised when a built-in operation or function receives an argument of the correct type but an inappropriate value.\n",
"4. `FileNotFoundError`: Raised when a file or directory is requested but cannot be found.\n",
"\n",
"Some examples that can cause different exceptions include:\n",
"\n",
"```python\n",
"print(1 / 0)\n",
"```\n",
"```\n",
"ZeroDivisionError: division by zero\n",
"```\n",
"\n",
"```python\n",
"print(int('abc'))\n",
"```\n",
"```\n",
"ValueError: invalid literal for int() with base 10: 'abc'\n",
"```\n",
"\n",
"\n",
"In Python, exceptions follow a structured inheritance hierarchy, forming a conceptual tree that represents their relationships. This hierarchy is instrumental in understanding the various types of exceptions and how they relate to one another. Think of it as an organized family tree for Python's exception classes.\n",
"\n",
"To explore this hierarchy comprehensively, you can refer to the complete structure here. This visual representation provides insights into the parent-child relationships among different exception classes, showcasing the inheritance patterns that define the Python exception system.\n",
"\n",
"\n",
"\n",
"### Exception Handling\n",
"\n",
"Python offers an elegant mechanism for dealing with exceptions. You might wonder why bother with it. However, handling exceptions in Python is essential for numerous reasons, enhancing the overall robustness, reliability, and maintainability of your code. Let's explore key reasons why handling exceptions is so important:\n",
"\n",
"1. **Preventing Program Crashes:** Without proper exception handling, unanticipated errors can cause your program to crash. Handling exceptions allows you to gracefully manage errors, preventing the entire program from terminating unexpectedly.\n",
"2. **User-Friendly Error Messages:** Exception handling enables you to provide meaningful and user-friendly error messages. This helps users or developers understand what went wrong, making it easier to identify and fix issues.\n",
"3. **Graceful Degradation:** In the presence of unexpected conditions, well-handled exceptions allow your program to degrade gracefully. Instead of abruptly stopping, your application can take appropriate actions, log the error for later analysis, or prompt the user for corrective input.\n",
"\n",
"\n",
"#### `try` - `except` statement\n",
"\n",
"The primary mechanism for handling exceptions in Python is the `try` and `except` block. Let's start from the classic example:\n",
"```python\n",
"while True:\n",
" try:\n",
" a = int(input(\"Enter integer: \"))\n",
" b = int(input(\"Enter integer: \"))\n",
" result = a / b\n",
" print(\"result of division is:\", result)\n",
" break\n",
" except ZeroDivisionError:\n",
" print(\"You're trying to divide by zero \")\n",
"```\n",
"\n",
"Let's break down the code snippet step by step:\n",
"\n",
"\n",
"* The code starts with an infinite loop, denoted by `while True`. This means the code inside the loop will keep executing indefinitely until a `break` statement is encountered.\n",
"Within the loop, there's a `try` block. The code inside this block is the main body of the loop that attempts to execute without error\n",
"Inside the `try` block, the user is prompted to enter two integers using input statements.\n",
"\n",
"* The entered values are converted to integers (`int()`), and if the user provides non-integer input, a `ValueError` will be raised.\n",
"After successfully obtaining two integers (`a` and `b`), the code proceeds to perform a division operation.\n",
"If the user enters `0` for `b`, a `ZeroDivisionError` will be raised.\n",
"If all the above steps are executed without encountering any errors, the code prints the result of the division:\n",
"After printing the result, the `break` statement is encountered, which exits the loop.\n",
"\n",
"\n",
"* If a `ZeroDivisionError` occurs during the division operation (`result = a / b`), the control is transferred to the `except ZeroDivisionError` block.\n",
"In this case, the program prints a message indicating that the user is attempting to divide by zero.\n",
"After handling the exception, the loop continues, prompting the user to enter integers again.\n",
"The user keeps entering values until a valid division operation is performed (i.e., a non-zero denominator is provided), and the `break` statement is executed, exiting the loop.\n",
"\n",
"#### Multiple `except` blocks\n",
"\n",
"In our previous example, we effectively addressed the situation of `ZeroDivisionError`. However, it's noteworthy that our program would crash if confronted with a `ValueError`. To mitigate this, we can enhance our error handling by incorporating multiple except blocks in our program:\n",
"\n",
"```python\n",
"while True:\n",
" try:\n",
" a = int(input(\"Enter integer: \"))\n",
" b = int(input(\"Enter integer: \"))\n",
" result = a / b\n",
" print(\"result of division is:\", result)\n",
" break\n",
" except ZeroDivisionError:\n",
" print(\"You're trying to divide by zero \")\n",
" except ValueError:\n",
" print(\"Some of the input couldn't be converted to integer number\")\n",
"```\n",
"\n",
"\n",
"#### `else` and `finally` statements\n",
"\n",
"\n",
"We can include additional statements to the `except` blocks:\n",
"\n",
"1. The `else` block is executed if no exceptions are raised in the `try` block.\n",
"2. The `finally` block is always executed, whether an exception occurs or not. It is useful for cleanup operations.\n",
"\n",
"```python\n",
"try:\n",
" a = int(input(\"Enter integer: \"))\n",
" b = int(input(\"Enter integer: \"))\n",
" result = a / b\n",
" print(\"result of division is:\", result)\n",
"except ZeroDivisionError:\n",
" print(\"You're trying to divide by zero \")\n",
"else:\n",
" print(\"There were no exceptions\")\n",
"finally:\n",
" print(\"Finally block\")\n",
"```\n",
"\n",
"\n",
"\n",
"#### Exceptions Hierarchy\n",
"\n",
"The exception hierarchy in Python is a structured organization of exception classes that represent various types of errors. This hierarchy is designed to provide a systematic way to handle and categorize exceptions based on their relationships. The hierarchy can be visualized as a tree, with a base class at the root and specialized exception classes branching out from it.\n",
"\n",
"> The full hierarchy of exceptions could be found here\n",
"\n",
"Let's delve into the Python exception hierarchy with code examples:\n",
"\n",
"```python\n",
"while True:\n",
" try:\n",
" a = int(input(\"Enter integer: \"))\n",
" b = int(input(\"Enter integer: \"))\n",
" result = a / b\n",
" print(\"result of division is:\", result)\n",
" break\n",
" except Exception:\n",
" print(\"General Exception happened\")\n",
" except ZeroDivisionError:\n",
" print(\"You're trying to divide by zero\")\n",
"```\n",
"\n",
"The noteworthy aspect in this example is that if there is any exception that is a subclass of `Exception`, it won't be reached. Therefore, it is essential to consider the precedence of exceptions you intend to handle. Careful consideration of exception order ensures that specific exceptions are caught and processed before more general ones, leading to effective and precise error handling.\n",
"\n",
"Typically, it's advisable to begin with more specific exceptions and place more general ones at the end of the except block. This ensures that the program prioritizes handling specific error conditions before addressing more general cases, contributing to a more nuanced and effective exception-handling strategy.\n",
"\n",
"```{caution}\n",
"It's a good idea to not try to deal with specific types of issues like `SystemExit`, `KeyboardInterrupt`, `GeneratorExit`, and `AssertionError`. Handling these might cause unexpected problems, so it's better to let them be and not interfere with how the program normally works. This helps maintain the program's stability and expected behavior.\n",
"```\n",
"\n",
"### Raise an exception\n",
"\n",
"We can manually raise exceptions using the `raise` statement. This is helpful when a specific condition is not met, and you want to signal an error."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba3079b4",
"metadata": {},
"outputs": [],
"source": [
"def validate_age(age):\n",
" if age < 0:\n",
" raise ValueError(\"Age cannot be negative\")\n",
" return age\n",
"\n",
"try:\n",
" user_age = validate_age(-5)\n",
"except ValueError as e:\n",
" print(f\"Validation error: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "75785ab2",
"metadata": {},
"source": [
"Here we specify the situation inside the `validate_age` function where we raise a `ValueError` exception. Additionally, we provide a custom message for our exception.\n",
"In case if a `ValueError` is raised during the execution of the `try` block (which happens when the age is negative), the control transfers to the `except` block.\n",
"The code inside the `except` block prints a message, including the error message from the raised `ValueError`.\n",
"You can see that we used the syntax `ValueError as e`, which allows us to refer to the exception object later on inside the `exception` block.\n",
"\n",
"\n",
"### User-defined exceptions\n",
"\n",
"We can craft our custom exceptions through inheritance. To illustrate, let's create an exception inheriting from the base `Exception` class:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "45431ea9",
"metadata": {},
"outputs": [],
"source": [
"class BioSeqException(Exception):\n",
" def __init__(self, msg):\n",
" self.msg = msg\n",
" def __str__(self):\n",
" return self.msg"
]
},
{
"cell_type": "markdown",
"id": "49e3b8c6",
"metadata": {},
"source": [
"Now, let's develop a class where we can apply our custom exception:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d5421e9",
"metadata": {},
"outputs": [],
"source": [
"def check_seq(seq):\n",
" if set(seq) <= set([\"A\", \"C\", \"G\", \"T\"]):\n",
" return\n",
" raise BioSeqException(\"Sequence should contain only ACGT symbols\")\n",
"\n",
"class BioSeq:\n",
" def __init__(self, seq):\n",
" check_seq(seq)\n",
" self.seq = seq"
]
},
{
"cell_type": "markdown",
"id": "f8128e6c",
"metadata": {},
"source": [
"Attempting to create a sequence containing symbols outside of ACGT will trigger a `BioSeqException`:\n",
"\n",
"```python\n",
"s = BioSeq(\"ACY\")\n",
"```\n",
"\n",
"```none\n",
"BioSeqException: Sequence should contain only ACGT symbols\n",
"```\n",
"\n",
"This way, we've defined a custom exception, `BioSeqException`, and utilized it in our `BioSeq` class to enforce constraints on the allowed sequence symbols. This enhances code clarity and facilitates the handling of specific error conditions related to biological sequences."
]
}
],
"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.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}