Handling Exceptions in Python

Errors are a part of every programmer’s life and knowing how to deal with them is a skill on its own.

The way Python deals with errors is called ‘Exception Handling’.

If some piece of code runs into an error, the Python interpreter will raise an exception.

Types of Exceptions

Let’s try to raise some exceptions on purpose and see the exceptions they produce.

  • TypeError

First, try to add a string and an integer

'I am a string' + 32
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be str, not int
  • IndexError

Now, try to access an index that doesn’t exist in a list.

A common mistake is to forget that sequences are 0-indexed, meaning the first item has index 0, not 1.

In this example, the list car_brands ends at index 2.

car_brands = ['ford', 'ferrari', 'bmw']
print(car_brands[3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
  • NameError

If we try to print a variable that doesn’t exist.

print(my_variable)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'my_variable' is not defined
  • ZeroDivisionError

Math doesn’t allow division by zero, trying to do so will raise an error, as expected.

32/0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

This was just a sample of the kinds of exceptions you might see on your daily routine and what can cause each of them.

Exception Handling

Now we know how to cause errors that will crash our code and shows us some message saying something is wrong.

To handle these exceptions just make use of the try/except statement.

try:
  32/0
except:
  print('Dividing by zero!')
Dividing by zero!

The example above shows the use of the try statement.

Put the block of code that may cause an exception inside the try scope, if everything runs alright, the except block is not invoked, but if an exception is raised, the block of code inside the except is executed.

This way the program doesn’t crash and if you have some code after the exception, it will keep running if you want to.

Specific Exception Handling

In the last example the except block was generic, meaning it was catching anything.

The good practice it to specify the type of exception we are trying to catch, it helps a lot when debugging the code later.

If I know a block of code can throw an IndexError, specify it in the except:

try:
  car_brands = ['ford', 'ferrari', 'bmw']
  print(car_brands[3])
except IndexError:
  print('There is no such index!')
There is no such index!

You can use a tuple to specify as many exceptions types as you want in a single except:

try:
  print('My code!')
except(IndexError, ZeroDivisionError, TypeError):
  print('My Excepetion!')

else

It is possible to add an else command in the end of the try/except. It runs only if there are no exceptions.

my_variable = 'My variable'
try:
  print(my_variable)
except NameError:
  print('NameError caught!')
else:
  print('No NameError')
My variable
No NameError

Raising Exceptions

The raise command allows you to manually raise an exception.

This is particularly useful if you want to catch an exception, do something with it, usually log the error in some personalized way like redirecting it to a log aggregator, or end the execution of the code since the error should not allow the progress of the program.

try:
  raise IndexError('This index is not allowed')
except:
  print('Doing something with the exception!')
  raise
Doing something with the exception!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
IndexError: This index is not allowed

finally

The finally block is executed independent of exceptions being raised or not.

They are usually there to allow the program to clean up resources like files, memory, network connections, etc.

try:
  print(my_variable)
except NameError:
  print('Except block')
finally:
  print('Finally block')
Except block
Finally block