Python Exceptions (Ausnahmebehandlung)

What Are Exceptions in Python?

When an unexpected condition is encountered while running a Python code, the program stops its execution and throws an error. There are basically two types of errors in Python: syntax errors and exceptions. To understand the difference between these two types, let’s run the following piece of code:

print(x
print(1)

A syntax error was thrown since we forgot to close the parenthesis. This type of error is always raised when we use a statement that is syntactically incorrect in Python. The parser shows the place where the syntax error was detected by a little arrow ^. Notice also that the subsequent line print(1) was not executed since the Python interpreter stopped working when the error occurred.

Let’s fix this error and re-run the code:

print(x)
print(1)

Now when we have fixed the wrong syntax, we got another type of error: an exception. In other words, an exception is a type of error that occurs when a syntactically correct Python code raises an error. An arrow indicates the line where the exception occurred, while the last line of the error message specifies the exact type of exception and provides its description to facilitate debugging. In our case, it is a NameError since we tried to print the value of a variable x that was not defined before. Also in this case, the second line of our piece of code print(1) was not executed because the normal flow of the Python program was interrupted.

To prevent a sudden program crash, it is important to catch and handle exceptions. For example, provide an alternative version of the code execution when the given exception occurs. This is what we are going to learn next.

Standard Built-in Types of Exceptions

Python provides many types of exceptions thrown in various situations. Let’s take a look at the most common built-in exceptions with their examples:

NameError – Raised when a name doesn’t exist among either local or global variables:

print(x)

TypeError – Raised when an operation is run on an inapplicable data type:

print(1+'1')

ValueError – Raised when an operation or function takes in an invalid value of an argument:

print(int('a'))

IndexError – Raised when an index doesn’t exist in an iterable:

print('dog'[3])

IndentationError – Raised when indentation is incorrect:

for i in range(3):
print(i)

ZeroDivisionError – Raised at attempt to divide a number by zero:

print(1/0)

ImportError – Raised when an import statement is incorrect:

from numpy import pandas

AttributeError – Raised at attempt to assign or refer an attribute inapplicable for a given Python object:

print('a'.sum())

KeyError – Raised when the key is absent in the dictionary:

animals = {'koala': 1, 'panda': 2}
print(animals['rabbit'])

For a full list of Python built-in exceptions, please consult the Python Documentation.

Handling Exceptions in Python

Since raising an exception results in an interruption of the program execution, we have to handle this exception in advance to avoid such undesirable cases.

The try and except Statements

The most basic commands used for detecting and handling exceptions in Python are try and except.

The try statement is used to run an error-prone piece of code and must always be followed by the except statement. If no exception is raised as a result of the try block execution, the except block is skipped and the program just runs as expected. In the opposite case, if an exception is thrown, the execution of the try block is immediately stopped, and the program handles the raised exception by running the alternative code determined in the except block. After that, the Python script continues working and executes the rest of the code.

Let’s see how it works by the example of our initial small piece of code print(x), which raised earlier a NameError:

try:
    print(x)
except:
    print('Please declare the variable x first')

print(1)

Now that we handled the exception in the except block, we received a meaningful customized message of what exactly went wrong and how to fix it. What’s more, this time, the program didn’t stop working as soon as it encountered the exception and executed the rest of the code.

In the above case, we anticipated and handled only one type of exception, more specifically, a NameError. The drawback of this approach is that the piece of code in the except clause will treat all types of exceptions in the same way, and output the same message Please declare the variable x first. To avoid this confusion, we can explicitly mention the type of exception that we need to catch and handle right after the except command:

try:
    print(x)
except NameError:
    print('Please declare the variable x first')

Handling Multiple Exceptions

Clearly stating the exception type to be caught is needed not only for the sake of code readability. What’s more important, using this approach, we can anticipate various specific exceptions and handle them accordingly.

To understand this concept, let’s take a look at a simple function that sums up the values of an input dictionary:

def print_dict_sum(dct):
    print(sum(dct.values()))

my_dict = {'a': 1, 'b': 2, 'c': 3}
print_dict_sum(my_dict)

Trying to run this function, we can have different issues if we accidentally pass to it a wrong input. For example, we can make an error in the dictionary name resulting in an inexistent variable:

def print_dict_sum(dct):
    print(sum(dct.values()))

my_dict = {'a': 1, 'b': 2, 'c': 3}
print_dict_sum(mydict)

Some of the values of the input dictionary can be a string rather than numeric:

def print_dict_sum(dct):
    print(sum(dct.values()))

my_dict = {'a': '1', 'b': 2, 'c': 3}
print_dict_sum(my_dict)

Another option allows us to pass in an argument of an inappropriate data type for this function:

def print_dict_sum(dct):
    print(sum(dct.values()))

my_dict = 'a'
print_dict_sum(my_dict)

As a result, we have at least three different types of exceptions that should be handled differently: NameError, TypeError, and AttributeError. For this purpose, we can add multiple except blocks (one for each exception type, three in our case) after a single try block:

def print_dict_sum(dct):
    print(sum(dct.values()))

#my_dict = 'a'

try:
    print_dict_sum(mydict)
except NameError:
    print('Please check the spelling of the dictionary name')
except TypeError:
    print('It seems that some of the dictionary values are not numeric')
except AttributeError:
    print('You should provide a Python dictionary with numeric values')

In the code above, we provided a nonexistent variable name as an input to the function inside the try clause. The code was supposed to throw a NameError but it was handled in one of the subsequent except clauses, and the corresponding message was output.

We can also handle the exceptions right inside the function definition. Important: We can’t handle a NameError exception for any of the function arguments since in this case, the exception happens before the function body starts. For example, in the code below:

def print_dict_sum(dct):
    try:
        print(sum(dct.values()))
    except NameError:
        print('Please check the spelling of the dictionary name')
    except TypeError:
        print('It seems that some of the dictionary values are not numeric')
    except AttributeError:
        print('You should provide a Python dictionary with numeric values')

print_dict_sum({'a': '1', 'b': 2, 'c': 3})
print_dict_sum('a')
print_dict_sum(mydict)

The TypeError and AttributeError were successfully handled inside the function and the corresponding messages were output. Instead, because of the above-mentioned reason, the NameError was not handled properly despite introducing a separate except clause for it. Therefore, the NameError for any argument of a function can’t be handled inside the function body.

It is possible to combine several exceptions as a tuple in one except clause if they should be handled in the same way:

def print_dict_sum(dct):
    try:
        print(sum(dct.values()))
    except (TypeError, AttributeError):
        print('You should provide a Python DICTIONARY with NUMERIC values')

print_dict_sum({'a': '1', 'b': 2, 'c': 3})
print_dict_sum('a')

The else Statement

In addition to the try and except clauses, we can use an optional else command. If present, the else command must be placed after all the except clauses and executed only if no exceptions occurred in the try clause.

For example, in the code below, we tried the division by zero:

try:
    print(3/0)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')

The exception was caught and handled in the except block and hence the else clause was skipped. Let’s take a look at what happens if we provide a non-zero number:

try:
    print(3/2)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')

Since no exception was raised, the else block was executed and output the corresponding message.

The finally Statement

Another optional statement is finally, if provided, it must be placed after all the clauses including else (if present)
and executed in any case, whether or not an exception was raised in the try clause.

Let’s add the finally block to both of the previous pieces of code and observe the results:

try:
    print(3/0)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')
finally:
    print('This message is always printed')
try:
    print(3/2)
except ZeroDivisionError:
    print('You cannot divide by zero')
else:
    print('The division is successfully performed')
finally:
    print('This message is always printed')

In the first case, an exception was raised, in the second there was not. However, in both cases, the finally clause outputs the same message.

Raising an Exception

Sometimes, we may need to deliberately raise an exception and stop the program if a certain condition occurs. For this purpose, we need the raise keyword and the following syntax:

raise ExceptionClass(exception_value)

Above, ExceptionClass is the type of exception to be raised (e.g., TypeError) and exception_value is an optional customized descriptive message that will be displayed if the exception is raised.

Let’s see how it works:

x = 'blue'
if x not in ['red', 'yellow', 'green']:
    raise ValueError

In the piece of code above, we didn’t provide any argument to the exception and therefore the code didn’t output any message (by default, the exception value is None).

x = 'blue'
if x not in ['red', 'yellow', 'green']:
    raise ValueError('The traffic light is broken')

We ran the same piece of code, but this time we provided the exception argument. In this case, we can see an output message that gives more context to why exactly this exception occurred.

Conclusion

In this tutorial, we discussed many aspects regarding exceptions in Python. In particular, we learned the following:

  • How to define exceptions in Python and how they differ from syntax errors
  • What built-in exceptions exist in Python and when they are raised
  • Why it is important to catch and handle exceptions
  • How to handle one or multiple exceptions in Python
  • How different clauses for catching and handling exceptions work together
  • Why it’s important to specify the type of exception to be handled
  • Why we can’t handle a NameError for any argument of a function inside the function definition
  • How to raise an exception
  • How to add a descriptive message to a raised exception and why it is a good practice

Nächste Einheit: 07 Grafische Ausgaben