Code 15: Python Exceptions 101

Del ving into Python Exceptions, In-Builts vs Custom Exceptions, Assert, Raise and Exception-Handlers
coding
python
Author

Tony Phung

Published

November 5, 2024

0. Introduction

Python follows the :

  • “Easier to Ask for Forgiveness than Permission” (EAFP) philosophy:
    • encouraging developers to try actions and
    • handle exceptions
      • if they fail,
      • rather than checking every condition beforehand.

1. Unexpected things happen!

1.1 Common Errors

Unexpected things or errors will occur at times in Python (and in life).

They might happen so often they get their own names and get categorised.

Python has done exactly that. Creating names for specific errors whilst maintaining Python’s hierarchy structure too. Some common (automatic) errors are:

  • Invalid syntax/code:
    • E.g. missing closing bracket, unexpected colons, or periods etc (SyntaxError exception)
  • Invalid username or key:
    • E.g. not existing in the database (KeyError exception)
  • Invalid calculation or operation:
    • E.g. dividing by zero or dividing a number by a letter (ZeroDivisionError, TypeError exception)
  • Invalid index:
    • E.g. indexing a value larger than the array/list length (IndexError exception)
  • Invalid attribute or method:
    • E.g. calling an non-existent function from an instance (AttributeError exception)
  • ….the list goes on… (pun intended)

1.2 Python In-Built (Automatic) Exceptions

All the above exceptions, being automatically raised by Python, are known as In-Built exceptions.

That is, when something unexpected happens, Python will:

  • Automatically halt the program,
  • Automatically raise or create a specific type Exception object (related to the error),
    • E.g. Syntax, KeyError, ZeriDivisionError, TypeError, IndexdError exception object
  • Automatically look for something that can deal with this specific type of exception,
    • called an Exception Handler (EH)
  • If no EH in current scope, Python looks for EH in call-stack,
    • If no EH in call-stack, the program is terminated.

Exceptions can also manually raised by the developer (discussed later).

2. Exception Handlers (EH)

EH are created with Try-Except statements:

  • to catch and
  • handle errors.

2.1 Example 1

try:
    print(1/0)
except ZeroDivisionError as e:
    print(f"An specific error has occured: [{e}]")
An specific error has occured: [division by zero]

2.2 Example 2

cool_dict = {"a": 420, "b":69}

try:
    a_val = cool_dict["a"]
    b_val = cool_dict["b"]
    c_val = cool_dict["c"]
except KeyError as e:
    print(f"KeyError caught: [{e}]")
else:
    print(f"Code has run succesfully!")
KeyError caught: ['c']

2.3 Example 3

try:
    print(1/"chode")
except TypeError as e:
    print(f"TypeError caught: [{e}]")
TypeError caught: [unsupported operand type(s) for /: 'int' and 'str']

2.4 Example 4

cool_list = [666,420,69]

try:
    print(cool_list[0])
    print(cool_list[1])
    print(cool_list[2])
    print(cool_list[3])
except IndexError as e:
    print(f"Index caught: [{e}]")
666
420
69
Index caught: [list index out of range]

2.5 Example 5

mad_int = 69

try:
    mad_int.append(420)
except AttributeError as e:
    print(f"Attribute error caught: [{e}]")
Attribute error caught: ['int' object has no attribute 'append']

3. Raising an Exception

How can a developer know all the exceptions?

  • and handle them all (when building software)?
  • It seems hard to be able to predict or prepare for all the different exceptions that occur?

Python is an inherited language:

  • All in-built are in fact sub-classes Exception class:
  • And the Exception class is a sub-class of the BaseException class

A developer can raise an exception when a condition is met. This condition could be:

  • boolean logic (True, False)
  • comparison logic (<, >, ==, >=, <=, !)
  • an python in-built exception

Then once the exception is caught, it can be handled in an except clause. This whole process is known as the Exception-Handler (EH)

3.1 Bare except clause

A bare except clause:

  • Python catches any exception that inherits from Exception (most built-in exceptions!)
  • Catching parent class Exception will:
    • Hides all errors— even unexpected or previously unseen errors!

3.1.1 Bare except clause example

try:
    with open("file.log") as file:
        read_data = file.read()
except:
    print("Couldn't open file.log")
Couldn't open file.log

3.2 except Exception clause

By catching Exception as e, there are attributes the developer can use:

  • The error occured: specific information about the error
  • The error type: the specific class of error
  • The error trace-back: a detailed trace-back of the error
try:
    with open("file.log") as file:
        read_data = file.read()
except Exception as e:
    import traceback
    print(f"[Error Occured 1/3]: \n\t[  {e}  ]\n")
    print(f"[Error Type 2/3]: \n\t[  {type(e).__name__}  ]\n")
    print(f"[Error Traceback 3/3]: \n\t[  {traceback.format_exc()}  ]")
[Error Occured 1/3]: 
    [  [Errno 2] No such file or directory: 'file.log'  ]

[Error Type 2/3]: 
    [  FileNotFoundError  ]

[Error Traceback 3/3]: 
    [  Traceback (most recent call last):
  File "/tmp/ipykernel_76566/3916192492.py", line 2, in <module>
    with open("file.log") as file:
  File "/home/tonydevs/.local/share/virtualenvs/blog-T-2huGx2/lib/python3.10/site-packages/IPython/core/interactiveshell.py", line 324, in _modified_open
    return io_open(file, *args, **kwargs)
FileNotFoundError: [Errno 2] No such file or directory: 'file.log'
  ]

3.3 Raising an in-built exception

def squared(numbers):
    if not isinstance(numbers, list | tuple):
        raise TypeError(
            f"list or tuple expected, got '{type(numbers).__name__}'"
        )
    return [number**2 for number in numbers]

4. assert keyword

  • Use only when debugging program during development.
  • This exception is the AssertionError.
  • The AssertionError is special:
    • It is not manually raised by the developer using raise.
    • Unlike every other manually raised exceptions.

4.1 Original Method: manually raise an Exception() (when condition met)

Below example raises an exception when a specific value is above arbitrary value (e.g. 5)

This exception can only be manually raised because it is:

  • not a syntax error exception
  • not an in-built error exception

In a way, this is more like:

  • model-error (e.g. specific to some real-world model specification)
  • business-logic (e.g. business application)
# number = 1
number = 6
if number > 5:
    raise Exception(f"[Manual Exc Raised & Caught]: The number should not exceed 5. ({number=})")
print(number)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[9], line 4
      2 number = 6
      3 if number > 5:
----> 4     raise Exception(f"[Manual Exc Raised & Caught]: The number should not exceed 5. ({number=})")
      5 print(number)

Exception: [Manual Exc Raised & Caught]: The number should not exceed 5. (number=6)

4.2 Another Method: Automated AssertionError with assert (when condition met)

number = 1
assert(number < 5), f"[Manual Exc Raised & Caught]: The number should not exceed 5. ({number=})"
print(number)
1
# example: https://realpython.com/python-exceptions/
def linux_interaction():
    import sys # https://docs.python.org/3.10/library/sys.html
    if "linux" not in sys.platform: 
        raise RuntimeError("Function can only run on Linux systems.")
    print(f"Running on a Linux system: [{sys.platform}]")
linux_interaction()
Running on a Linux system: [linux]
# example: https://realpython.com/python-exceptions/
def windows_interaction():
    import sys # https://docs.python.org/3.10/library/sys.html
    if "windows" not in sys.platform: 
        raise RuntimeError("Function can only run on Windows systems.")
    print(f"Running on a Windows system: [{sys.platform}]")
windows_interaction()
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[12], line 7
      5         raise RuntimeError("Function can only run on Windows systems.")
      6     print(f"Running on a Windows system: [{sys.platform}]")
----> 7 windows_interaction()

Cell In[12], line 5, in windows_interaction()
      3 import sys # https://docs.python.org/3.10/library/sys.html
      4 if "windows" not in sys.platform: 
----> 5     raise RuntimeError("Function can only run on Windows systems.")
      6 print(f"Running on a Windows system: [{sys.platform}]")

RuntimeError: Function can only run on Windows systems.

5. Custom Exceptions

This Custom Exception are exceptions that the developer can create in-built exceptions may not be suitable.

Scenarios may require them:

  • maybe a file doesn’t exist,
  • a network or database connection fails,
  • or code gets invalid input.

5.1 Create Custom Exception (CE)

CE are created by the:

  • class NameOfCustomException(Exception): use class constructor and inherit from Exception.

  • raise exception in a function defintion (when condition is met):

    • with no EH: call function outside try-except (exception is not handled): Program terminates.
    • with an EH: call function in try-except (exception is handled): Program continues.

5.2 Raising CE without EH

5.2.1 Example 1: PlatformException

class PlatformException(Exception):
    """Incompatible platform."""
    pass

def linux_interaction():
    import sys
    if "linux" not in sys.platform:
        # raise RuntimeError("Function only for Linux systems.")    # previous-code: in-built exception
        raise PlatformException("Function only for Linux systems.") # updated-code: custom exception     
    print("Doing Linux things.")
    
linux_interaction()
Doing Linux things.

5.2.2 Example 2: GradeValueError

class GradeValueError(Exception):
    pass

def calculate_average_grade(grades):
    total = 0
    count = 0
    for grade in grades:
        if grade < 0 or grade > 100:
            raise GradeValueError(
                "grade values must be between 0 and 100 inclusive"
            )
        total += grade
        count += 1
    return round(total / count, 2)

print(calculate_average_grade([80,70,-90]))
print("Exception is not handled, Program is terminated by Python. This line is not printed")
---------------------------------------------------------------------------
GradeValueError                           Traceback (most recent call last)
Cell In[14], line 16
     13         count += 1
     14     return round(total / count, 2)
---> 16 print(calculate_average_grade([80,70,-90]))
     17 print("Exception is not handled, Program is terminated by Python. This line is not printed")

Cell In[14], line 9, in calculate_average_grade(grades)
      7 for grade in grades:
      8     if grade < 0 or grade > 100:
----> 9         raise GradeValueError(
     10             "grade values must be between 0 and 100 inclusive"
     11         )
     12     total += grade
     13     count += 1

GradeValueError: grade values must be between 0 and 100 inclusive

5.3 Raising CE with EH

5.3.1 GradeValueError (EH less verbosity)

Captured Error output with less verbosity. This may be suitable and it may not.

try:
    GPA = calculate_average_grade([80,70,-90])
except GradeValueError as e:
    print(f"Captured Error: [{type(e).__name__}]:\n\t[{e}]\n")
    # import traceback
    # print(f"Traceback here: \n\t{traceback.format_exc()}")
else:
    print(f"Congrats, your gpa is {GPA}")
print(f"Finished Grading!")
Captured Error: [GradeValueError]:
    [grade values must be between 0 and 100 inclusive]

Finished Grading!

5.3.2 GradeValueError (Exception-Handler more verbosity)

By using traceback, the verbose output could also be provided.

try:
    GPA = calculate_average_grade([80,70,-90])
except GradeValueError as e:
    print(f"Captured Error: [{type(e).__name__}]:\n\t[{e}]\n")
    import traceback
    print(f"Traceback here: \n\t{traceback.format_exc()}")
else:
    print(f"Congrats, your gpa is {GPA}")
print(f"Finished Grading!")
Captured Error: [GradeValueError]:
    [grade values must be between 0 and 100 inclusive]

Traceback here: 
    Traceback (most recent call last):
  File "/tmp/ipykernel_76566/3646266675.py", line 2, in <module>
    GPA = calculate_average_grade([80,70,-90])
  File "/tmp/ipykernel_76566/2024016610.py", line 9, in calculate_average_grade
    raise GradeValueError(
GradeValueError: grade values must be between 0 and 100 inclusive

Finished Grading!

6. Multiple Exceptions

def division(a, b):
    try:
        return {
            'success': True,
            'message': 'OK',
            'result': a / b
        }
    except (TypeError, ZeroDivisionError, Exception) as e:
        return {
            'success': False,
            'message': str(e),
            'type': type(e).__name__,
            'result': None
        }

result1 = division(10,10)
result2 = division(10, 0)
result3 = division("A", 10)
print(result1)
print(result2)
print(result3)
{'success': True, 'message': 'OK', 'result': 1.0}
{'success': False, 'message': 'division by zero', 'type': 'ZeroDivisionError', 'result': None}
{'success': False, 'message': "unsupported operand type(s) for /: 'str' and 'int'", 'type': 'TypeError', 'result': None}