Handling Errors and Exceptions in Python

Exception handling in Python is performed using the try except code block. This tutorial covers errors and exception handling with a set of examples.
Andrew Wood  •   22 June 2022
Andrew Wood  •   Last Updated: 22 June 2022

Introduction

In order to create robust, well constructed code (we should all be aiming for this), it is necessary to provide a means of dealing with exceptions that may come about when your code is run.

An exception, or exceptional event, refers to anomalous or exceptional conditions that in general break the flow of normal code execution, and could lead to an error if not caught and dealt with.

There is a subtle difference between an error and an exception in relation to coding.

  • An error is a condition in the code that can't be handled at runtime and will almost certainly cause the code to fail. An error in syntax for example will cause the code to fail during parsing to the compiler.
  • An exception indicates a deviation from the intended manner in which your code should be run, but doesn't necessarily mean that the code is rendered inoperable after an exception is detected.  

Exception handling refers to the deliberate act of responding to an exception as it is raised. It often involves highlighting the reason for the exception, and providing a description of how to avoid the exception in the future. In some cases the handing of the exception will allow the procedure in which the exception was raised to continue, for example a mathematical result that is undefined could be handled by providing an alternate value when the exception is raised. 

Syntax Errors

A syntax error occurs when there is an error detected in the way a sequence of characters or section of code is written that makes it unreadable to the compiler. Syntax errors therefore are detected during compiling and not at runtime.

Python is very good at providing the developer with clear guidelines as to where the error in syntax occured. Errors in syntax raise a SyntaxError.

In the list below I have left a comma out between the final two entries in the list. A SyntaxError is automatically raised when the code is executed with a description of where the error occured.

broken_list = ['a','b' c] # missing comma after 'b'
File "C:\Users\andre\AppData\Local\Temp\ipykernel_15008\2218430382.py", line 7
    broken_list = ['a','b' c]
                           ^
SyntaxError: invalid syntax

Syntax Errors in Strings

Another example of a common syntax error is the misuse of single and double quotation marks when working with string. Take a look at the example below where I have used a single quotation to start my string, but then included the word "I'm" in the string. The single quotation mark in "I'm" casues the string to terminate prematurely, raising a SyntaxError. This is an easy error to resolve; simply use double quotation marks to start and terminate the string when apostrophes are used in the string body.

print('I've messed my commas up in this string')
  File "C:\Users\andre\AppData\Local\Temp\ipykernel_15008\818652324.py", line 1
    print('I've messed my commas up in this string')
             ^
SyntaxError: invalid syntax

 To correct the error use double quotation marks to start and end the string.

print("I've messed my commas up in this string")

Types of Exceptions

There are many built-in exception types that can be raised by Python interpreter. For a complete list refer to the official Python docs. We'll go over four common exception errors encounted, namely: 

  • NameError
  • TypeError
  • IndexError
  • ValueError

NameError Exception

NameError exception is raised when a local or global name is not found. The associated error message that is raised includes the name that couldn't be found. Commonly seen when a variable is misspelled.

# NameError Example
name = 'Bob'
print(Name) # variable should be lowercase
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_15008\515426119.py in <module>
      1 # NameError Example
      2 name = 'Bob'
----> 3 print(Name)

NameError: name 'Name' is not defined

TypeError Exception

TypeError is raised when an operation or function is applied to an object of inappropriate type. The error message will include a string with details about the type mismatch.

# TypeError
name = 'Bob'
age = 24
print(name + 'is ' + age + 'years old.') # age is not a string
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_15008\529589267.py in <module>
      1 name = 'Bob'
      2 age = 24
----> 3 print(name + 'is ' + age + 'years old.')

TypeError: can only concatenate str (not "int") to str

IndexError

An IndexError is raised when a sequence subscript is out of range. A good example would be referring to an index in a list that does not exist.

# IndexError
mycars = ['BMW','Audi','VW','Tesla']
myfavoritecar = mycars[4]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_15008\2496475245.py in <module>
      1 mycars = ['BMW','Audi','VW','Tesla']
----> 2 myfavoritecar = mycars[4]

IndexError: list index out of range

ValueError

ValueError is raised when an operation or function receives an argument that is of the correct type but an inappropriate value.

# ValueError
import math
a = -16
print(math.sqrt(a))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_15008\3695188383.py in <module>
      2 import math
      3 a = -16
----> 4 print(math.sqrt(a))

ValueError: math domain error

Handling Exceptions with a Try Except Block 

Now that we've introduced a few of the more common exceptions, let's go ahead and run through how we deal with them in our codebase. Python's exception handling offers us a try except block which we can add to our code in places where we believe an exception may be raised.

  • The try portion of our block will always be executed.
  • The except portion will only be executed if an exception is raised. 
mycars = ['BMW','Audi','VW','Tesla']
idx = 5

try: # this will always run
    idrive = mycars[idx]
    print(f"Today I'll drive my {idrive}!")
except: # this will only run if an exception is raised
    print("You don't have that many cars!")

In the code block above we have called an index that doesn't exist in the mycars list. As a result an IndexError will be raised, the code will jump to the except statement, and the statement in the except block will be printed.

"You don't have that many cars!"

Catching Particular Exceptions

In the example above we knew that the exception raised would be of the type IndexError as we purposely called an index that wasn't in the list. You can add multiple except blocks if you wish to perform different actions for different errors raised. Let's rewrite the block to differentiate between IndexErrors, TypeErrors, and all other exceptions.

# Add multiple exceptions to catch different error types
mycars = ['BMW','Audi','VW','Tesla']
idx = 3

try:
    idrive = mycars[idx]
    print(f"Today I'll drive my {idrive}!")
except IndexError as e:
    print("You don't have that many cars!")
    print(f"Error: {e}")
except TypeError as e:
    print("You've entered the wrong data type!")
    print(f"Error: {e}")
except Exception as e: # catches all other errors
    print("This catches all other errors.")
    print(f"Error: {e}")

If we now force a TypeError we will print a type specific error to the screen. This is useful as it both alerts the user to what has gone wrong, and also allows the developer to modify the behaviour of the code depending on what sort of exception is raised.

Python's built-in exception class provides further details on the exception which we can extract from the except using the syntax:

except Exception as e:
print(e)

If we now send a str as the list index lookup instead of an int we will raise a TypeError with a clear message as to what went wrong.

idx = '2' # string but should be an int
"You've entered the wrong data type!"
"Error: list indices must be integers or slices, not str"

Nested Exceptions & Modifying Returned Values

Raising an exception doesn't have to mean that the process will fail. We can modify the result that returned an error and allow the process to continue. Let's look at a simple divider function, that takes two values as inputs, and divides the one by the other. 

We know that if the denominator is 0 then a ZeroDivisionError will be raised, and so we can use this to instead return None and allow the process to continue.

numerator = 4
denom = 0

def divider(numerator,denom):
    try:
        value = numerator/denom
    except ZeroDivisionError as e:
        value = None # assign None to the result
        print(e)
    except TypeError:
        try:
            value = float(numerator)/float(denom)
        except Exception as e:
            print(f"You've entered the wrong data type!: {e}")
    except Exception as e:
        print(f"Exception Raised: {e}")
    return value

divi = divider(numerator,denom)
print(f"{numerator} divided by {denom} = {divi}.")

If you take another look at the divider function you should notice that we have nested a second try except block within the TypeError exception. We know that the exception raised has to do with the incorrect datatype being input into the divider so we can try to force a conversion of the two inputs into floats which would allow the divider to continue to run if the conversion is possible. This adds some robustness to the code and allows for example, a string denom = '4' to be accepted as a valid input. This is only meant as an example to show how exceptions can be nested; in this case it may be preferable to force the float conversion in the initial try statement (a denom='0' input would still fail with the current implementation of the nested block).

numerator = 4
denom = 0
>>> divi = divider(numerator,denom)
>>> print(f"{numerator} divided by {denom} = {divi}.")
"division by zero"
"4 divided by 0 = None."

numerator = 4
denom = '2' # this is now a str
>>> divi = divider(numerator,denom)
>>> print(f"{numerator} divided by {denom} = {divi}.")
"4 divided by 2 = 2.0."

Else & Finally: Increasing Try Except Functionality

There are two additional optional statements that are available to us when exception handling: the else and finally statements. Let's discuss these now and show how and when we should use them.

  • try: test a block of code for errors.
  • except: handle any errors raised.
  • else: execute code when there is no error. 
  • finally: execute code regardless of the results of the try except block.

The else block is designed to be executed if no errors are raised. If an exception is raised then the else block will not run. There is nothing stopping you from adding additional code in your try block rather than in the else block - however, it may improve the readibility of your code to use the else block if you have many extra lines to add before the first except

The finally block will be run regardless of what happens in the try and except blocks. This is useful if you need to clean up your code irrespective of whether an exception was raised or not. A common use case for the finally block is to close an open file after an exception is raised during file parsing. In this instance, adding the close() method to the finally block ensures that the file is always closed. 

I've extended our car list example to make use of an else and finally statement.

# try, except, else, finally example
mycars = ['BMW','Audi','VW','Tesla']
idx = 3

try:
    idrive = mycars[idx]
    print(f"Today I'll drive my {idrive}!")
except IndexError:
    print("You don't have that many cars!")
except TypeError:
    print("You've entered the wrong data type!")
except:
    print("This catches all other errors.")
else:
    print("I'm going to the mall.")
finally:
    print(f"I have {len(mycars)} cars in my garage.")
mycars = ['BMW','Audi','VW','Tesla']
# calling a valid index
idx = 3

"Today I'll drive my Tesla!"
"I'm going to the mall." # else statement printed as no exception raised
"I have 4 cars in my garage." # finally statement will already run

# calling an invalid index
idx = 5
"You don't have that many cars!"
"I have 4 cars in my garage." # finally statement will already run

Raising Your Own Exceptions

There may be instances where you need to raise your own exception in a situation where an exception would otherwise not be raised. A good example is if you wanted to limit access or entry based on a variable such as age.

Raising an exception is simple to do using the raise block.

age = 14
legal_age = 18

if age < legal_age:
    raise Exception("You are too young to buy alcohol!")
else:
    print("Welcome to the bottle store.")

Running the code above raises an exception and outputs the following:

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_21836\2277537898.py in <module>
      3 
      4 if age < legal_age:
----> 5     raise Exception("You are too young to buy alcohol!")
      6 else:
      7     print("Welcome to the bottle store.")

Exception: You are too young to buy alcohol!

Wrapping Up

Exception handling is an important Python concept and allows you to raise, catch, and handle exceptions as they occur. Exceptions are different to syntax errors in the sense that syntax errors are raised during compiling while exceptions are raised at runtime.

The following concepts were discussed in this tutorial.

  • We looked at a number of different types of exceptions and how to independently handle the different types of exceptions raised.
  • We discussed the try except block which is the foundation of exception handling in Python.
  • We extended the try except block to include else and finally statements.
    • else will only run if no exceptions are raised.
    • finally will always run and is used to clean up code. 
  • We finished off by looking at the raise statement which is used when you need to specify your own conditions to raise an exception. 

Thanks for reading this tutorial. Hopefully it has been useful and will encourage you to practice your own exception handling in your next coding project.  

Share this
Comments
Canard Analytics Founder. Python development, data nerd, aerospace engineering and general aviation.
Profile picture of andreww
Share Article

Looking for a partner on your next project?

Contact Us