Intro to Programming: How to Handle Errors

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.
By Ciprian Stratulat • Updated on Mar 16, 2022

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, we'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.

We have already seen an example of a possible error in your code: we asked a user for a number and, instead, they gave us some text. In such a scenario, our whole program would crash. It would, of course, be much better if we 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. We typically think of something as being an exception if it's out of the ordinary. In programming, an exception is typically something bad that we did not predict. Or, more accurately, an exception is an error that occurs while the program is running. We have already seen some of them throughout this series, but today we will address them more formally.

Exceptions are not necessarily issues with our overall logic. They can occur simply because we did not foresee some unique way in which a user of our program would use the code that we wrote. Fortunately for us, the Python interpreter is quite helpful when exceptions occur. Exceptions not only have a message that informs us 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 we're trying to add a number to a string, but that's not possible. We know that we can use the plus operator to merge two strings, and we 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, we say that an exception is thrown. Python's very helpful and tells us both what kind of an exception we 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, we'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 we know such exceptions are possible, our 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.

The Try-Except Construct

We 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, we use indentation to represent code that is inside some other structure. We have to use indentation when we 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 we use to refer to this whole construct, so don't be confused if you hear it. Anyways, in the try clause, we specify the code that we 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, we follow the keyword except with a colon sign, and below, we write the block of code that we want to run in case of exceptions. Just as before, we 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 we write the code that we want to attempt to run, and which may or may not throw exceptions. In the except clause, we write the code that we 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.

 

The Try-Except-Finally Construct

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 we 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 we jump to write 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 we mentioned on our slides. Let's write print(2 + "hello"). When we run this, we see that an exception is thrown. Now, let's handle this exception and print the message Can't do that instead. We start by writing the keyword try, followed by a colon, and then hit enter and inside the try clause we'll write print(2 + "hello") and then finally, we 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 we run this, we no longer see the TypeError from before, but instead, we see the message Can't do that. We just handled our first exception. Because our 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 we again just simply write print(2 + "hello") and below it we write print('hi'), when we 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 we run this, we 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, we can add another print statement inside the except clause, let's say print('sorry'). If we run this, we'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 we really want to print that line that says hi? Well, we 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 we run this, we see the message Can't do that, but right below it we 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 we 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 we run this, we 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 our previous example, except that now instead of print(2 + "hello"), we write print(2 + 2), which is a perfectly valid computation. So our code is now: try: print(2 + 2) except: print("Can't do that"). If we run this code, we 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 our 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 we run this, we see the result of 2 + 2, which is 4, but we also see the phrase I can do math. That's because the latter was inside the finally clause, which always gets executed, whether we have an exception or not.

So far, what we've done here is handle all exceptions that occur in our 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. We can reuse the example we had earlier. So we know that if we attempt to print(2 + "hello") we'll get a TypeError exception, as we saw earlier. If we want to only handle this kind of exception, we 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 we did earlier, except that now, after the keyword except, we wrote specifically what kind of exception we 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 video, we'll look at 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, we're basically trying to print the variable my_variable that hasn't been defined. Because we haven't defined it anywhere, Python doesn't know it by name, so it will throw a NameError exception. However, we're prepared for this because in our except clause we're handling the NameError exception and we're 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: we're basically again trying to print my_variable, which is still undefined, and we know this will result in a NameError exception being thrown as we saw earlier. Now we're also trying to print the result of adding the integer 2 and the string hello, which we know 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, we're coding defensively here, so we're prepared to handle both exceptions. If the NameError exception is thrown, we're going to print the message Who is my_variable?. If a TypeError exception is thrown, we're going to print the message Can't do that. If we run this code, we 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 we now run this code, we see that print(2 + "hello") gets executed and results in a TypeError exception being thrown. This is then handled by our 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 our next article, where we discuss files and wrap up our series.

Ciprian Stratulat

CTO | Software Engineer

Ciprian Stratulat

Ciprian is a software engineer and the CTO of Edlitera. As an instructor, Ciprian is a big believer in first building an intuition about a new topic, and then mastering it through guided deliberate practice.

Before Edlitera, Ciprian worked as a Software Engineer in finance, biotech, genomics and e-book publishing. Ciprian holds a degree in Computer Science from Harvard University.