NB: Exception Handling

Programming for Data Science

Errors

An error is anything that causes a program to stop working.

Anything in the realm of writing and running code, that is — we are not considering here such things as power and hardware failures.

Errors may be classified into a variety of broad types:

  • Syntactic errors, AKA parsing errors, which are caused by not following Python’s rules for writing code. For example, you start a variable name with a number.

  • Logical errors, such as dividing by 0 or indexing into a list for an element that doesn’t exist.

  • User errors, such as when a user inputs the wrong data type for an operation.

  • System errors, such as when a database connection is down or a write operation is blocked by incorrectly set file permissions.

  • Dependency errors, such as when a module expects a version of another library that is not installed.

And so on.

Some of errors are in your control and some are not.

You can control how you write and design your code.

And you, or your team, can often control what software is installed and running on the system running your code.

But you can’t control how users or external systems will behave.

In cases where errors are in your control, you should update your code to eliminate them.

In cases where they are out of your control, you should anticipate and handle them so that your code may continue to execute.

Exceptions

The official Python documentation defines an exception as follows:

Errors detected during execution are called exceptions.

This excludes syntactic errors and dependency errors and focuces on problems that arise when code is running and data is being processed.

However, this definition does not make a distinction between what’s in an out of your control.

Exception Handling

In practice, errors that are out of your control require that you make exceptions for them.

Anticipating and working around these errors is called exception handling.

To handle an exception means two things:

  • To provide a way of catching it

  • To provide a response when it is caught

Python’s Tools

Python provides tools for identifying errors and handling them when they arise.

Exceptions are represented as a collection of Python object that classify errors.

For example, a ValueError is one in which an incorrect value is used for a given operation.

Exception Handlers are control structures and functions to deal with exceptions. These include:

  • try/except
  • raise
  • assert

Some Common Built-in Exceptions

Python comes with a number of predefined exceptions, in addition to ValueErrror.

Modules introduce new ones, too. For example, a database connection module might include exceptions for a broken connection or for a SQL constraint violation.

Exceptions are used in a raise statement by the authors of programs when a possible error is anticipated.

Here are some common examples.

ZeroDivisionError

3 / 0
ZeroDivisionError: division by zero

SyntaxError

An if-statement missing colon at end:

if x > 0
  print("uh oh")
SyntaxError: expected ':' (2612676770.py, line 1)

NameError

This code references an undefined variable:

print(x)
NameError: name 'x' is not defined

IndexError

This loop goes beyond the length of the list:

lst = [0, 1, 2]

for i in range(4):
    print(lst[i])
0
1
2
IndexError: list index out of range

You can see that when Python encounters errors, it stops running and prints an error message detailing the error type and its location in code.

You should get used to reading these messages. Although they may be off-putting and suffer from TMI, they do point to the specific line in your code where the error was encountered.

Exception Handling with try/except

Python provides try/except blocks to handle exceptions in uyour code.

These blocks work as follows:

First, the try block will contain a statement.

Then, if the statement inside the block fails, the flow switches to the except block.

The exception block will have code to handle the error, rather than halting the program.

The process is very similar to if/then:

  • If there is an error, then raise an excpetion.

Multiple except statements may be given, to handle specific exceptions.

Below, we give a catch-all except for any kind of exception.

print(a)
NameError: name 'a' is not defined

Now let’s try referencing a variable that doesn’t exist with a try/except block to handle the exception.

try:
    print(a)
except:
   print("caught an exception")
caught an exception

We could also do this:

try:
    print(a)
except NameError as e:
    print(e)
name 'a' is not defined

Here we print the message, but the program continues to run.

else and finally

Exception handling blocks may also contain else and finally statements.

else statements run if no errors are caught.

finally statements are run no matter what happens. This is used in cases where variables or connections are set up in the process of testing and need to be deleted or closed.

Here is a an example of a function that contains a fully qualified exception handling block:

def divide(x, y): 
    try: 
        result = x / y 

    except ZeroDivisionError: 
        print("Error: Can't divide by 0!") 
    
    else:
        print("Result:", result) 
    
    finally:  
        print('This is always executed')   
divide(3, 2) 
Result: 1.5
This is always executed
divide(3, 0)
Error: Can't divide by 0!
This is always executed

Assert Statements

Another way to catch errors is to test if computations turned out as expected.

We use assert to verify if an expression is True.

If an expression is True, nothing happens.

If an expression is False, Python raises an AssertionError exception.

Assert statements have the following syntax:

assert Expression, Message

The message is optional. It is printed if the expression is False.

For example, consider a program that is expecting three arguments to be passed from the command line.

Assume the variable num_args represents the number of arguments.

num_args = 3
assert num_args == 3, "number of arguments must be 3!"

Here, the assert evaluates to True, and things proceed normally without exception.

If we change num_args = 4 this will throw an AssertionError with the provided message.

The program then stops.

num_args = 4
assert num_args == 3, "number of arguments must be 3!"
AssertionError: number of arguments must be 3!

If the assert is not given a message, it throws AssertionError: without a message.

num_args = 4
assert num_args == 3
AssertionError: 

You can mix assert statements withtry/except blocks.

try:
    assert num_args == 3
except:
    print("Got an error")
Got an error

Note that the except message will append the assert message in this case.

try:
    assert num_args == 3, "Assert: Number of arguments must be 3!"
except AssertionError as e:
    print('Except:', e)
Except: Assert: Number of arguments must be 3!

raise

Exceptions can be raised, too.

When using raise, you must pass it an exception type function.

The argument of the function is a message.

In this example, we want to stop the program if the code encounters a ZeroDivisionError.

x = 0 # Some value given by a user

if x == 0:
    raise ZeroDivisionError("Hey, you can't divide by zero!")
ZeroDivisionError: Hey, you can't divide by zero!

Summary

Exceptions are errors that arise in a running program that are expected to happen.

We handle exceptions with try and except statements.

raise and assert statements are ways to make your code responsive to errors that may arise.

We sometimes use these to actually stop program flow so that users of our code can catch them and handle them.