Exceptions

Notes Single

For building a more robust code, it's crucial managing the possible exceptions instead watching how the application breaks one day when an external resource (for example, a database or an API) is not reachable or when the user enters a not allowed value. During this article, we will check the different kinds of exceptions which can be managed and real examples of how we can apply those solutions.

What is an exception?

An exception represents an error or indicates something is going wrong during our code execution. Let's mention some examples:

1. If we need to do a division, the divisor can't be zero, in that case, an exception called ZeroDivisionError will be raised, and our mission as developers is to prevent it causes our application breaks, managing the different paths we can take for keeping the execution over control. For example, if the divisor is introduced by the user, we can send a message to the user indicating "0" is not an allowed value. In the same example, if the user introduces a letter instead of a number, it will raise an exception called TypeError, in that case, we could send a message to the user indicating the value introduced is not processable.

2. If our application must consume an API but in the determined moment that API is off-line, our application could generate an unexpected behavior while waiting for the answer of API. Then, we could prevent it by managing the possibility the API doesn't answer and determining what actions will execute our application in this case.

Most common exceptions in Python

First of all, let's know what are the most common exceptions to manage in Python and a short description of them:

  1. Exception: The base class for all exceptions.
  2. SyntaxError: Raised when a syntax error occurs during parsing.
  3. IndentationError: Raised when there is an indentation-related syntax error.
  4. NameError: Raised when a local or global name is not found.
  5. TypeError: Raised when an operation or function is applied to an object of an inappropriate type.
  6. ValueError: Raised when a function receives an argument of the correct type but an inappropriate value.
  7. KeyError: Raised when a dictionary key is not found.
  8. IndexError: Raised when a sequence subscript is out of range.
  9. FileNotFoundError: Raised when a file or directory is requested but cannot be found.
  10. IOError: Raised when an I/O operation fails.
  11. ZeroDivisionError: Raised when division or modulo by zero occurs.
  12. AssertionError: Raised when an assert statement fails.
  13. ImportError: Raised when an import statement fails to find the module or name.
  14. NotImplementedError: Raised when an abstract method that should be implemented in a subclass is not actually implemented.
  15. KeyboardInterrupt: Raised when the user interrupts the execution, usually by pressing Ctrl+C.

Now, we need to know those (above) are built-in exceptions, they are exceptions defined in the language and raised at the moment that specific conditions are detected. It exists an alternative for raising custom exceptions, this will be shown further in this article.

How to manage the exceptions?

In Python, the block try-except is used for exception handling. It allows you to catch and handle exceptions that may occur during the execution of a specific block of code. The try block is where you place the code that you think may raise an exception. If an exception occurs within the try block, the execution of the block is immediately halted, and the program flow is transferred to the except block. The except block is where you define the exception handling code. It specifies the type of exception you want to catch and how you want to handle it. You can have multiple except blocks to handle different types of exceptions. Here's an example of a complete try-except block with various conditions:

try:
    # Code that may raise exceptions
    num1 = int(input("Enter the numerator: "))
    num2 = int(input("Enter the denominator: "))
    result = num1 / num2

    # Other operations that may raise exceptions
    myList = [1, 2, 3]
    index = int(input("Enter an index: "))
    value = myList[index]
    print("Value:", value)

except ValueError:
    print("Invalid input. Please enter a valid integer.")
except ZeroDivisionError:
    print("Error: Division by zero occurred!")
except IndexError:
    print("Error: Index is out of range!")
except Exception as e:
    print("An error occurred:", str(e))
else:
    print("No exceptions occurred.")
finally:
    print("This block always executes, regardless of exceptions.")

The following example shows an infinite loop, which is broken with the "Break" instruction when it receives the appropriate data, as long as this does not happen and therefore the capture of different error possibilities is executed, the indicated message will be displayed to each case:

while True:
    try:
        var1 = int(input('Introduce a number: '))
        var2 = int(input('Introduce a number: '))
        division = var1/var2
        print('The division result is ' + str(division))
        break
    except ValueError as eValueType:
        print('Error: ' + str(eValueType))
    except ZeroDivisionError as eZero:
        print('Error: ' + str(eZero))
    except:  # Any other error
        print('Error detected')
    finally:
        print('The algorithm was executed')

Custom exceptions

As we mentioned before, we can raise a custom exception, for this, we must define that exception inheriting from BaseException (or Exception), this will provide us the basic behaviors as Exception and we must use the statement raise for activating the exception at the convenient condition:

class CustomException(BaseException):
    pass

try:
    # Code that may raise the custom exception
    raise CustomException("This is a custom exception")

except CustomException as e:
    print("Error:", str(e))

Let's check some code which will raise exceptions

General exception

SyntaxError

There is a semicolon after the element "2" in the list, which is an err in the syntaxis, it will raise a SystaxError

list = [1, 2; 3]
element = list[10]

A SyntaxError can't be caught or handled by an exception handling structure, it is an exception thrown by the Python interpreter when it encounters a syntax error in the source code during the lexical or parsing stage.

Because a SyntaxError occurs before the actual execution of the code, it can't be caught or handled at runtime. The Python interpreter will stop the execution of the program and display an error message indicating the location and type of syntax error encountered.

IndentationError

The second line does not have a correct indentation and it raises an IndentationError:

for element in range(5):
print(element)

As in the previous case, the IndentationError can't be handled in a try-except block either, it must be manually corrected.

NameError

If we try to use a variable which is not define, it will raise a NameError.

print(my_variable)

and the answer will be:

NameError: name 'my_var' is not defined

Here, an example about how to handle the exception:

try:
    print(my_variable)
except NameError as e:
    print("NameError occurred:", e)

TypeError

The substraction is not a valid operation for strings, it will raise a TypeError exception

result = 'abc' - 'g'

Caching the exceptions:

try:
    result = 'abc' - 'g'
except TypeError:
    print('This operator is not valid for strings')

ValueError

value = int("123.45")

Managing the exception:

try:
    value = int("123.45")
except ValueError:
    print('This value can't become integer')

KeyError

IndexError

The program tries to access to an index wich does not exist and it raises an IndexError stopping the excecution:

list = [1, 2, 3]
element = list[10]

The same situation but managing the exception:

try:
    list = [1, 2, 3]
    element = list[10]
except IndexError:
    print("Index not valid")

FileNotFoundError

IOError

ZeroDivisionError

AssertionError

ImportError

NotImplementedError

KeyboardInterrupt

Real cases

Handling exceptions in an API connection

Let's check how we can handle possible exceptions in case the connection to an API fail:

import requests
from requests.exceptions import Timeout, ConnectionError

url = "https://api.chucknorris.io/jokes/random"

try:
    response = requests.get(url, timeout=5)
    # Process the response data
    print("API response:", response.json())

except Timeout:
    print("Error: Request timed out. The API did not respond within the specified timeout.")

except ConnectionError:
    print("Error: Failed to establish a connection with the API.")

except Exception as e:
    print("An error occurred:", str(e))

In the above, code we handle 3 possible situations:

  • The API exceeds the specific time for replying.
  • There is a connection error.
  • Any other exception raised.

Handling exceptions in a Database connection

In this case, if there is a problem with the Database connection, an OperationalError will be raised:

import psycopg2
from psycopg2 import OperationalError

try:
    # Establish a database connection using with statement
    with psycopg2.connect(
        host="localhost",
        database="mydatabase",
        user="myuser",
        password="mypassword"
    ) as connection:
        # Execute database operations
        cursor = connection.cursor()
        cursor.execute("SELECT * FROM mytable")
        rows = cursor.fetchall()

        # Process the retrieved data
        for row in rows:
            print(row)

except OperationalError as e:
    print("Error: Failed to connect to the database.")
    print("Details:", str(e))

except Exception as e:
    print("An error occurred:", str(e))

Thanks for reading :)
I invite you to continue reading other entries and visiting us again soon.

Related Posts: