Python Exceptions: Try-Except to handle errors

Python exceptions allows you to better handle your program in case of error, find out why with this tutorial

Share This Post

Share on linkedin
Share on facebook
Share on twitter
Share on email

Starting with functions, and then with classes, you can now create complex scripts. These scripts will handle complex data, and you need to be careful they don’t break. In fact, a script that deals with some data needs to know what to do when these data are wrong. We simply can’t assume the user will give correct data. While you can already prevent errors with your knowledge, today we will learn how to make your code ready to handle errors. To do that, we are going to use a great feature: Python Exceptions, or the famous try ... except construct.

Where do errors come from?

When I was first approaching coding, I couldn’t see why a code could generate an error. In reality, the reasons for generating an error are plenty, and many of them are obvious. Of course, a code can contain bugs, that generates unexpected errors, but this is not the only possibility. In fact, the common case to have an error is when the program is dealing with unexpected data.

When dealing with Python exceptions, you need to understand possible causes of problems. Here we have some: bugs, corrupted files and bad user input.
Possible causes for an error to arise.

The classical example is the division by zero, which is not possible. No number, multiplied by 0, can generate a number different than zero: it is simply impossible. Thus, imagine we have the following code.

number_1 = int(input("Write the first number: "))
number_2 = int(input("Write the second number: "))

print("Result is " + str(number_1/number_2))

This works like a charm, as long as the user keeps giving us what we expect. Instead, if he gives us 0 as a second number, it will generate an error.

Looking at our first error

If we execute the code from the previous snippet, with zero as the second number, this will be the result.

C:\Users\aless\Desktop>python myscript.py
Write the first number: 18
Write the second number: 0
Traceback (most recent call last):
  File "myscript.py", line 4, in 
    print("Result is " + str(number_1/number_2))
ZeroDivisionError: division by zero

Don’t get scared by this message, it is extremely simple to understand. You can see the error message has four lines. We can analyze them in sequence.

  1. Traceback (most recent call last): is simply indicating we have an error. Since we have an error at the root of our code, we will see just these four lines. But imagine you have a function, that calls a function, that calls another function and so on. You will have several lines of messages to identify when the error happened. We will see that later.
  2. File "myscript.py", line 4, in <module> tells the file that generated the error and the module. Since we haven’t defined any module name, the module is replaced by the string <module>.
  3. print("Result is " + str(number_1/number_2)) is simply the line of code where the error happened.
  4. ZeroDivisionError: division by zero tells you which type of error we encountered (ZeroDivisionError) and an explicative message (division by zero).

An error nested in a function

It is very unlikely that you will see errors at the root of your scope. Most likely, errors will happen inside a function or class method that is probably called from other scopes as well. Here is the exact same code as above, but with several functions to show you the point.

def perform_division(num_1, num_2):
  return num_1/num_2
  
def division_as_string(num_1, num_2):
  return str(perform_division(num_1, num_2))
  
def get_division_results(num_1, num_2):
  return "The result is: " + division_as_string(num_1, num_2)

number_1 = int(input("Write the first number: "))
number_2 = int(input("Write the second number: "))

print(get_division_results(number_1, number_2))

Here the error will be a little longer, but yet without additional complexity.

C:\Users\aless\Desktop>python myscript.py
Write the first number: 18
Write the second number: 0
Traceback (most recent call last):
  File "myscript.py", line 13, in 
    print(get_division_results(number_1, number_2))
  File "myscript.py", line 8, in get_division_results
    return "The result is: " + division_as_string(num_1, num_2)
  File "myscript.py", line 5, in division_as_string
    return str(perform_division(num_1, num_2))
  File "myscript.py", line 2, in perform_division
    return num_1/num_2
ZeroDivisionError: division by zero

As we can see, the line indicating the file and the line indicating the piece of code generating the error are repeated. We start from the outer scope on top (line 13) to drill down until we reach the point where the error actually happened. This way you can quickly skim through your code. This is a lifesaver if you want to troubleshoot which function is passing parameters to the other in the wrong way.

Python exceptions to the rescue!

Preventing python exceptions

The errors you see are the python exceptions because the code is handling with something unexpected. You can write code that generates no exception, simply with if and else. Thus, we can rewrite our code above to prevent the error.

def division(num_1, num_2):
  if num_2 == 0:
    return "Cannot divide by 0"
  return num_1 / num_2
  
number_1 = int(input("Write the first number: "))
number_2 = int(input("Write the second number: "))

print("The result is: " + str(division(number_1, number_2)))

In case we attempt to give 0 as the second number, the script will recognize that before the error happens. This is a simple way to write code that deals with problems. It is completely okay, in fact, Google likes this coding style. However, this poses some serious limitations. Just continue reading to find out why.

Handling Python Exceptions

With the selection statement, you can only prevent exceptions. You cannot deal with them if they arise. Instead, to deal with them we have a special statement, the try .. except. This construct is simple: Python will attempt to execute the code in the try block. In case an exception arises, it will switch to the except block.

Try to change your division function to reflect the one below.

def division(num_1, num_2):
  try:
    return num_1 / num_2
  except:
    return "We had an error"

Every time the division is successful, the code inside except won’t be executed. Instead, if we put 0 in num_2, Python will execute that code instead of performing the division.

Right now, you may think there is no advantage when comparing to exception prevention. However, exceptions raise to the outer scope until they find a try ... except that handles them. This means we can take care of this error even in a different point of the code. We can change our script like the one below.

def division(num_1, num_2):
  return num_1 / num_2
  
number_1 = int(input("Write the first number: "))
number_2 = int(input("Write the second number: "))

try:
  print("The result is: " + str(division(number_1, number_2)))
except:
  print("Error while performing calculation")

The real advantage of handling Python Exceptions

Why on earth would you need to handle exceptions away from where they happen? Haha, this is a great question, and the answer comes with practice. In some circumstances, you want to have some control over the errors in the outer scope.

The reason for that is simply because handling exceptions is a core part of your code, not just something you have to do later. A common example is when creating a module, a library, or something that other developers will use. You want to give them the ability to react the way they want in case of error. Thus, you create functions that do not handle exceptions or only handle some of them. The same is true when you want to create these libraries for yourself. You want your library to be generic, so handling some exceptions inside of it might not be a great idea.

Remember, our goal is not to go completely silent on errors. Errors may still happen, and it is OK to present them to users (maybe in a more readable format). Instead, our main goal is to ensure that the program handles errors in the correct way, and for some of them, the right way is presenting them to the user. Think about it, in our last example, we didn’t fix the program. We simply told the user that he was giving a bad number_2.

Handling multiple exceptions

In the except statement, you can define which exception you wish to handle with it. This way, you can attach multiple except statements to the same try, so that each deals with a specific exception. This code is an example of that.

def division(num_1, num_2):
  return num_1 / num_2
  
try: 
  number_1 = int(input("Write the first number: "))
  number_2 = int(input("Write the second number: "))

  print("The result is: " + str(division(number_1, number_2)))
except ZeroDivisionError:
  print("Cannot divide by zero!")
except ValueError:
  print("Insert only integers!")
except:
  print("Generic error...")

As you can see, the last statement does not specify any specific exception to handle. Thus, it will catch any error that is not ZeroDivisionError nor ValueError. However, the Python standards recommend you to not use just except. Instead, you should always provide the error you are going to handle.

What if some error is not handled? You should know what are all the possible errors your code may generate, and handle them correctly with individual statements.

If you want, you can condense multiple errors in a single statement with brackets, like except(NameError, IndexError):.

Finally

Each try must have at least one except. However, you can also attach a finally statement after all your except statements. Python will execute the code here in any case, after the try (and the except, if it was triggered). You need this construct if, for example, you need to close files or similar stuff.

def division(num_1, num_2):
  return num_1 / num_2
  
try: 
  number_1 = int(input("Write the first number: "))
  number_2 = int(input("Write the second number: "))

  print("The result is: " + str(division(number_1, number_2)))
except ZeroDivisionError:
  print("Cannot divide by zero!")
except ValueError:
  print("Insert only integers!")
except:
  print("Generic error...")
finally:
  print("Finished!")

Working on the exception

In the except statement, you might actually need to work on the exception you received. For example, you might want to store its error in a file. You can do that by assigning the exception received to a variable, with the keyword as.

...
except ValueError as e:
  with open("errors.log", "a") as file:
    file.write("Error #" + str(e.errno))

In this snippet, we write the error number to a file. We do that by leveraging the errno property of every error. We can also find the message in the strerror property.

Creating Python Exceptions

If you can handle Python Exceptions, it is only because someone else created them. The ZeroDivisionError is embedded inside the division operand (/), for example. The ValueError can be triggered by the int() function, and so on. Like someone defined these exceptions, you can tell your code when to generate exceptions. Here we have a cool example.

def validate(day, month):
  if day < 1:
    raise ValueError("Day must be greater than 1")
  if month < 1 or month > 12:
    raise ValueError("Month must be between 1 and 12")
  try:
    if month == 1 or month == 3 or month == 5 or month == 7 or month == 8 or month == 10 or month == 12:
      if day > 31:
        raise ValueError
    elif month == 2:
      if day != 28 and day != 29:
        raise ValueError
    else:
      if day > 30:
        raise ValueError
  except ValueError:
    raise ValueError("Month " + str(month) + " cannot have " + str(day) + " days")

day = int(input("Day: "))
month = int(input("Month: "))

validate(day, month)

In this example, our script won’t output anything. Instead, it will only generate an error in case something is wrong. To generate an error, we use the keyword raise. The keyword is “raise” because errors are meant to be raised to the outer scope, and so on until they are handled. To that keyword, we need to add the name of the error and any parameters it requires. Remember that errors are just objects like others.

Explaining the good stuff

In our example, we start by checking the absolute validity of day and month. In case they are wrong, we quickly raise an error with a description (as the ValueError can accept that). However, the good stuff happens right after that.

In the second part of the function, we validate combination of month and day inside a try block. In case something is wrong, we simply raise a ValueError without description. The try block will then switch to the except block, and handle that error. Here we generate another error, and we add the message. This way we don’t need to write the same message three times. Pretty neat.

Custom Python Exceptions

Now you can raise existing Python exceptions, but sometimes you want to define your own. This is an advanced topic, as you need to define a class that inherits from another, and we haven’t talked about that yet. However, for now, just know that it is possible to have your own error with your own name. Simply use this construct.

class ValidationError(Exception):
  pass

As you can see, we simply define a class that gets its properties from the default Exception class. We name our class ValidationError, yet we don’t define anything and instead use the keyword pass. Once you will learn about inheritance, you will also learn about defining additional properties. As a naming convention, include “Error” in the name.

Default Python Exceptions

Python has a lot of pre-built errors. Below the ones you must know.

  • IndexError happens when you have problems with lists, like if you are trying to access an item that does not exist.
  • NameError is similar to the IndexError, but applies with dictionaries.
  • ValueError comes when something is not of the type it should be. Maybe it was a string but should have been an integer – something like that.
  • EOFError deals with the end of a file. If you are reading a file, it might be corrupted. Most likely, you missed some brackets in your python file and thus Python does not know when to close them.

Conclusion

Now that you know about python exceptions and how to handle them, you should be ready to write bug-free code. Don’t worry if your code is still buggy at the beginning, you will get better with time. A great exercise is challenging your code with unlikely inputs. Try writing some text where you want to see numbers or vice versa. Try adding spaces at the beginning or at the end of user inputs, try passing a file of a format instead of the expected one.

What do you think about Python Exceptions? How are you plan to use them? What was the most difficult think about them? Let me know in the comments!

Alessandro Maggio

Alessandro Maggio

Project manager, critical-thinker, passionate about networking & coding. I believe that time is the most precious resource we have, and that technology can help us not to waste it. I founded ICTShore.com with the same principle: I share what I learn so that you get value from it faster than I did.
Alessandro Maggio

Alessandro Maggio

Project manager, critical-thinker, passionate about networking & coding. I believe that time is the most precious resource we have, and that technology can help us not to waste it. I founded ICTShore.com with the same principle: I share what I learn so that you get value from it faster than I did.

Join the Newsletter to Get Ahead

Revolutionary tips to get ahead with technology directly in your Inbox.

Alessandro Maggio

2018-05-31T16:30:18+00:00

Unspecified

Python

Unspecified

Want Visibility from Tech Professionals?

If you feel like sharing your knowledge, we are open to guest posting - and it's free. Find out more now.