Table of Contents
Hi and welcome back to the newest article in my Intro to Programming series!
How to Handle Errors and Exception in Python
In this article, I'll be discussing errors and exception handling.
The first thing you need to be aware of is that errors are inevitable when writing code. Even with a lot of experience, you'll still have errors in your code. That's because writing computer code is a very complex task, and it's sometimes hard to predict all the ways in which your code will be used or abused.
As such, while you should aim to write the best code you can, it's also necessary to practice defensive coding. What I mean by that is that you should anticipate which parts of your code could be subject to possible errors, and you should write your code in a way that you can gracefully handle them.
You've have already seen an example of a possible error in your code: a user was asked for a number and, instead, they have written some text. In such a scenario, the whole program would crash. It would, of course, be much better if you could tell the user that their input is not a valid number instead of just letting the program crash.
I mentioned this word, exceptions, and I think it's a good time to define it now. Typically, an exception is something out of the ordinary. In programming, an exception is typically something bad that you did not predict. Or, more accurately, an exception is an error that occurs while the program is running. You have already seen some of them throughout this series, but today I will address them more formally.
Exceptions are not necessarily issues with your overall logic. They can occur simply because you did not foresee some unique way in which a user of your program would use the code that you wrote. Fortunately, the Python interpreter is quite helpful when exceptions occur. Exceptions not only have a message that informs you what went wrong, but they're also each labeled with a specific type.
For example, let's consider this code: print(2 + "hello"). Here, it seems that you're trying to add a number to a string, but that's not possible. You know that you can use the plus operator to merge two strings, and you can use the plus operator to add two numbers. But the plus operator can do nothing when it's given a number and a string. So this will result in an error. In programming lingo, they say that an exception is thrown. Python's very helpful and tells you both what kind of an exception you have - in this case, it's a TypeError exception - as well as more details about it: unsupported operand type(s) for +: 'int' and 'str'. In other words, you're trying to use the plus operator with two operands, one of which is an integer and the other a string, and that's unsupported.
When you know such exceptions are possible, your best course of action is to handle them.
Exception handling is the act of writing code that can detect exceptions and perform some pre-determined actions instead of just crashing. If exceptions are not explicitly detected and handled, the default behavior in Python is to simply crash your whole script.
What Is The Try-Except Construct?
You can handle exceptions using a try-except construct. In its most basic form, the try-except construct uses two keywords: try and except.
The try keyword is followed by a colon and then, below, by a block of code that will be attempted to be executed. By now, you should be familiar with the fact that, in Python, you use indentation to represent code that is inside some other structure. You have to use indentation when you write code that is inside a function, inside a branch of an if statement, or inside a loop. The same thing applies here. The code inside the try statement is indented by 4 spaces or 1 tab, whichever you prefer, and Python uses this indentation to distinguish between code that is inside the try statement and code that is outside of it.
I also want to make you aware that, sometimes, you'll hear the term try clause. That refers to the try keyword plus the code lines that follow it. It's just a term that they use to refer to this whole construct, so don't be confused if you hear it. Anyways, in the try clause, you should specify the code that you want to keep an eye on. This code will be run normally, line by line, and can contain if statements, for loops, function calls, etc.
If an exception happens while executing the code inside the try clause, Python will automatically jump and execute the code specified inside the except clause below. Here, too, you should follow the keyword except with a colon sign, and below, you should write the block of code that you want to run in case of exceptions. Just as before, you should indent this code by 4 spaces or 1 tab in order to allow Python to distinguish between code that's inside the except clause and code that's outside of it.
So to reiterate, inside the try clause you should write the code that you want to attempt to run, and which may or may not throw exceptions. In the except clause, you should write the code that you want to run if an exception DOES occur while running the code inside the try clause. If an exception doesn't happen, the code inside the except clause will NOT be run, so be careful about that.
How Does The Try-Except-Finally Construct Work?
There is also a slightly more complex construct called try-except-finally. This code structure is very similar to the try-except, but it has this one extra branch, the finally clause. Code that you write inside the finally clause will be run regardless of whether there is an exception or not. And just as before, pay attention to the colon following the keyword finally, as well as to the 4 spaces or 1 tab indentation that is used to specify which lines of code are inside the finally clause.
Before writing some code, another thing I want to make you aware of is that you can actually have nested try-except clauses. So, for example, in the except clause, you can start another try-except construct, to handle possible exceptions that might occur there. Also, exceptions that happen outside of a try clause will not be handled, so if some exception occurs in the except clause, your program will still crash. Only code inside a try clause is handled in case of exceptions.
And finally, before you go ahead and wrap your entire program in one giant try-except construct, you should know that good code practices require that you keep these constructs fairly small. In other words, they should be localized. It's generally better to have multiple try-except constructs in your program rather than one giant one. That has to do partially with code readability, and partially with the fact that you always want to handle the exception as close to where it happens as possible. So, if you have a 100-line program inside a try-except construct and an exception happens while executing line 2 but you only handle it on line 102, that's really not ideal.
Python Code for Handling Exceptions
Let's start with the simple example mentioned on these slides. Let's write print(2 + "hello"). When you run this, you see that an exception is thrown. Now, let's handle this exception and print the message Can't do that instead. You should start by writing the keyword try, followed by a colon, and then hit enter and inside the try clause you'll write print(2 + "hello") and then finally, you write the except clause: except: print("Can't do that").
# Let's try to run this print(2 + "hello") # We get the following error message: #--------------------------------------------------------------------------- #TypeError Traceback (most recent call last) #<ipython-input-1-eebc5fdf938c> in <module> #----> 1 print(2 + "hello") #TypeError: unsupported operand type(s) for +: 'int' and 'str' # Let's try a try-except construct try: print(2 + "hello") except: print("Can't do that") #We get the following output: # Can't do that
If you run this, you will no longer see the TypeError from before, but instead, you'll see the message Can't do that. You just handled your first exception. Because the program no longer crashes, code lines that follow the try-except construct will actually be executed. Let me show you what I mean by that.
If you again just simply write print(2 + "hello") and below it you write print('hi'), when you run this, you'll see that the word 'hi' is never printed. That's because print(2 + "hello") threw an exception, and the program immediately crashed.
# If we try to print hi: print(2 + "hello") print('hi') # We get the following error message: #--------------------------------------------------------------------------- #TypeError Traceback (most recent call last) #<ipython-input-3-65555127d273> in <module> #----> 1 print(2 + "hello") #2 print('hi') #TypeError: unsupported operand type(s) for +: 'int' and 'str'
Now, let's rewrite this and wrap it in a try clause, so it becomes:
# Let's adjust our try-except construct to include print('hi) try: print(2 + "hello") print('hi') except: print("Can't do that") # But out output is still # Can't do that
If you run this, you see the message Can't do that, but still no hi. Why is that? Well, when the code inside the try block executes, the line print(2 + "hello") throws an exception, and the exception is handled by the except clause, which prints the message Can't do that. However, after the exception is handled, the program does not resume at the line right below the one that caused the exception. Instead, it continues with the code that follows below the except clause.
For example, you can add another print statement inside the except clause, let's say print('sorry'). If you run this, you'll see the message Can't do that and, on a new line, Sorry.
# Let's create a new try-except construct try: print(2 + "hello") print('hi') except: print("Can't do that") print("Sorry") # Our output is now # Can't do that # Sorry
So what if you really want to print that line that says hi? Well, you have two options. Option number 1 is to add it inside a finally clause. So the code now becomes:
# Let's try a try-except-finally try: print(2 + "hello") except: print("Can't do that") finally: print('hi') # Our output is now: # Can't do that # hi
If you run this, you see the message Can't do that, but right below it you finally see the word hi.
Option 2 is to include the print('hi') code line outside of the whole try-except clause. Remember that once the exception is handled, the program resumes with the code that immediately follows below. So you can write:
# We could try putting print('hi) after the construct try: print(2 + "hello") except: print("Can't do that") print('hi') # And our output will be # Can't do that # hi
Notice here how there's no indentation before print('hi'). That's because this line of code is now entirely outside the try-except code construct. If you run this, you again see Can't do that on one line, and right below it, the word hi.
Now, I also want to reiterate the point that, if no exception occurs, whatever code lines you have in your except clause will NOT actually be executed. So let's go with the previous example, except that now instead of print(2 + "hello"), you write print(2 + 2), which is a perfectly valid computation. So the code is now: try: print(2 + 2) except: print("Can't do that"). If you run this code, you see printed the result of calculating 2 plus 2, but not the phrase Can't do that. That's because no exception was thrown, so therefore, the code block inside the except clause wasn't executed.
# Let's see what happens when there is no exception try: print(2 + 2) except: print("Can't do that") # Our output is: #4
However, as I mentioned earlier, if you do have the optional finally clause, whatever code you include in that clause will actually be executed. So, let's change this code slightly and add a finally clause. Let's say:
# Let's see what happens to our finally construct # without an exception try: print(2 + 2) except: print("Can't do that") finally: print("I can do math") # Our output is # 4 # I can do math
If you run this, you see the result of 2 + 2, which is 4, but you also see the phrase I can do math. That's because the latter was inside the finally clause, which always gets executed, whether you have an exception or not.
So far, what you've done here is handle all exceptions that occur in your try clauses. But Python also gives us the option of only handling specific kinds of exceptions. That's because, believe it or not, sometimes it's actually desirable to have your program crash after all, if, for example, some totally unexpected exception occurs and you don't know how to recover from it.
To only handle specific exceptions, by kind, all you have to do is specify it after the except keyword. Let's go over an example. You can reuse the earlier example. So it is known that if you attempt to print(2 + "hello") you'll get a TypeError exception, as you saw earlier. If you want to only handle this kind of exception, you can simply write:
# We can specify what kind of exception we want to handle: try: print(2 + "hello") except TypeError: print("Can't do that") # Our output is now: #Can't do that
This is very similar to what you've seen earlier, except that now, after the keyword except, you wrote specifically what kind of exception you wanted handled.
You can discover more exceptions that can be handled by looking at the Python documentation online. Also, if you use functions from third-party packages, the documentation for the specific packages should also specify in which situations those functions will throw exceptions, and what kind of exceptions they are. This will make it easier for you to handle them. In the next section, you'll learn how to handle multiple kinds of exceptions in the same try-except construct.
How to Handle Multiple Exceptions
If you're handling exceptions specifically by kind, you can actually have multiple except clauses. Let me show you an example. You'll have to trust me that NameError is another kind of exception that Python can throw. You'll see this exception, for example, when you try to use a variable that hasn't been assigned a value. Let's see it in action.
Let's write the following code:
# Let's handle a NameError exception try: print(my_variable) except NameError: print("Who is my_variable?") # Our output is # Who is my_variable?
Here, you are basically trying to print the variable my_variable that hasn't been defined. Because it isn't defined anywhere, Python doesn't know it by name, so it will throw a NameError exception. However, you are prepared for this because in your except clause you are handling the NameError exception and you are printing the message Who is my_variable?.
Now, let's change this code slightly and write:
# Let's handle two exceptions at once try: print(my_variable) print(2 + "hello") except NameError: print("Who is my_variable?") except TypeError: print("Can't do that") # Our output is #Who is my_variable?
Ok, so this is nothing new: you're basically again trying to print my_variable, which is still undefined, and you know this will result in a NameError exception being thrown as you saw earlier. Now you're also trying to print the result of adding the integer 2 and the string hello, which is not possible because the two operands - 2 and hello - are of different data types. So that line, if it was reached, would result in a TypeError exception.
But, here's the wonder of coding defensively: you're prepared to handle both exceptions. If the NameError exception is thrown, you're going to print the message Who is my_variable?. If a TypeError exception is thrown, you're going to print the message Can't do that. If you run this code, you see that only What is my_variable gets printed. Why is that? Well, remember that, after an exception is thrown and handled, code execution resumes with the code that follows below the except clause, and NOT with the code that was right below the line that caused the exception.
So, in this case, the line print(my_variable) throws an exception, which is a NameError exception. This exception gets handled with the print message, but the line print(2 + "hello") never gets executed, so the TypeError exception is never thrown.
Let's now switch the two lines of code, so instead of printing my_variable first, let's write print(2 + "hello") first. If you now run this code, you see that print(2 + "hello") gets executed and results in a TypeError exception being thrown. This is then handled by the second except clause so that the message Can't do that is printed on the screen. In this case, too, the line print(my_variable) doesn't get executed at all because the line above it threw an exception.
# Let's switch our exceptions and see what happens try: print(2 + "hello") print(my_variable) except NameError: print("Who is my_variable?") except TypeError: print("Can't do that") # The other except construct is run, and our output is #Can't do that
Now, when you first start writing code, you probably won't handle specific exceptions a whole lot and that's ok. Handling exceptions at all is much better than not handling them. However, as you get more advanced in your programming, it's always a good idea to handle specific kinds of exceptions. That's because handling a specific kind of exception gives you a lot of context that you can use to more accurately tell the user what's going on. We've all encountered online systems that crash unexpectedly with the generic message "Something went wrong". That's good to know, but even better would be to know what exactly went wrong and if I, as the user, can do something to fix it. Did I enter my birth date in the wrong format? Did I click on some button I wasn't supposed to click on? If the programmers had been less lazy and had handled individual exceptions, the messages we see would be far more insightful.
That's all for exceptions and how to handle them. Now that you have the tools to recover from exceptions, it's always good practice to look at the code you write and ask yourself: "How could this code fail?" If you can foresee ways in which it would fail and result in an exception being thrown, you should do the right thing and handle that exception.
Thanks for reading, and keep practicing! Stay tuned for the next article, where I discuss files and wrap up this series.