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
A 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
A 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
A 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 IndexError
s, TypeError
s, 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 float
s 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
andfinally
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.