Chapter 6.P

Python Exceptions

Exceptions in Python

Subsections of Python Exceptions

Common Exceptions

Before we learn about how to detect and handle exceptions in Python, let’s review some of the common exceptions and errors we may see in our programs. Each of the headers below is the name of an exception in Python, which is represented by a particular class in the Python programming language.

Exception

Every exception that can be handled in Python is a subtype of the Exception class. So, when we aren’t sure which type of exception to expect, we can always use the Exception class to make sure we catch all of them.

ArithmeticError

An ArithmeticError can occur whenever our program attempts to perform a calculation that would result in an error. This is the base class for various other errors, such as OverflowError and ZeroDivisionError

OverflowError

An OverflowError occurs when the result of a calculation cannot be represented correctly. Typically this exception is very rare, but can happen when working with older code or libraries.

ZeroDivisionError

A ZeroDivisionError is thrown whenever the program is asked to either divide by $0$ or perform the modulo operation with $0$ as the second input. Here’s an example:

x = 5 / 0 # throws ZeroDivisionError
y = 3 % 0 # throws ZeroDivisionError

IndexError

An IndexError happens when we try to access an array index that does not exist. Here’s a great example:

array = [1, 2, 3, 4, 5]
array[5] = 10 # throws IndexError

In this example, we are trying to access the 6th element in the array, which is at array index 5. However, since the size of the array is only 5, we’ll get an IndexError when we try to execute this code since there is no 6th element.

NameError

In Python, a NameError occurs when we try to use the name of a variable, class, or other item which Python can’t find. For example:

x = 5 + y # throws NameError

In this code, we are trying to use the variable y without first giving it a value. When we execute this code, it will throw a NameError.

FileNotFoundError

This exception occurs when the program is trying to open a file that does not exist. This is one of the more common errors that programmers must deal with when using files for input, and it is relatively simple to correct. In most cases, we can simply ask the user to provide another file. Here’s an example of some code that may cause this exception:

filename = ""
reader = open(filename) # throws FileNotFoundError

In this case, we are providing a blank filename, which causes the program to throw a FileNotFoundError.

TypeError

A TypeError occurs when we try to use a variable in a way that is not allowed based on the type of the variable. We’ve probably already seen this exception many times when working with string outputs. Here’s an example:

x = 5
print("Number: " + x) # throws TypeError

In this code, we are using the plus + operator to concatenate two strings together. However, the second variable, x, is an integer, so it is not the correct type for this operation. Therefore, the code will produce a TypeError when executed.

ValueError

A ValueError typically occurs when an input to a method is an acceptable type but has an incorrect value. Here are a couple of examples:

x = int("12.3") # throws ValueError
y = float("abc") # throws ValueError

In both of these examples, the int and float methods are provided with the correct type of input, a string. However, neither of those strings can be converted to the corresponding type, so a ValueError is thrown.

AssertionError

Python Assertions

An AssertionError happens when our code reaches an assertion that fails. An assertion is a check added by the programmer to verify that a particular situation doesn’t occur in the code, such as a variable being negative or an array being too large. Python supports the use of a special keyword assert that can be used to add these assertions to our code. By default, the Python interpreter enforces all assert statements, but they can easily be disabled by adding -O as an argument to the python3 command when running a program. This is a helpful step when debugging a new application. We’ll learn more about assertions and how they can be used effectively in a later chapter.

For now, here’s a quick example of an assertion that would produce an AssertionError:

x = 5
y = 3
assert x + y < 7 # throws AssertionError if enabled

Errors

Beyond exceptions, there are a few unrecoverable errors that may occur in our Python code. Recall that errors are special types of exceptions that should not be handled by our programs, and should instead be allowed to cause our programs to crash. Some examples are:

  • SyntaxError - occurs when the Python interpreter finds code with invalid syntax
  • IndentationError - occurs when the Python interpreter finds improperly indented code
  • SystemError - occurs when the Python interpreter encounters an internal error

Since Python is an interpreted language, these errors will occur when code is running, sometimes even after most of the program is executed. In general, we should not try to catch or correct these errors in our code. Instead, we should allow our program to crash, knowing that we’ll have to fix the underlying source code to solve the problem.

References:

Exceptions vs. Errors

Since Python is an interpreted language, it can be hard to tell the difference between exceptions and errors. So, let’s take a minute to review those two terms and what they mean.

Exceptions

An exception is any issue encountered while executing a program that can be resolved within the program itself. Typically most exceptions come from incorrect user input, but they can be caused by other issues as well. However, just because an exception can be resolved by the program does not automatically mean that the program does, or must, include a handler for that exception. It just means that it could handle the exception, if the developer included the appropriate handler and code.

Errors

As discussed before, errors are a special type of exception that cannot be easily handled by the program. These are usually due to issues in the underlying operating system or problems in the code itself. The Python interpreter may return errors such as SyntaxError or IndentationError relating to issues with the code, but it also may return errors such as SystemError or OSError if it encounters a problem that it cannot easily resolve.

Try-Except

YouTube Video

Video Materials

Programs written in Python can have many exceptions occur on a regular basis. Thankfully, as we learned earlier, it is possible to detect and handle these exceptions in our code directly, in order to prevent our program crashing and causing the user more stress. Let’s see how we can perform this step in Python.

Consider the following program, if provided a command-line argument, it treats that string as a file-name and opens it for reading. If there is no command-lie argument, it defaults to keyboard entry. It then attempts to get an input and convert that to an integer.

import sys

class Try:
    
    @classmethod
    def main(cls, args):
        if len(args) > 1:
            reader = open(args[1])
        else:
            reader = sys.stdin
        x = int(reader.readline())
        print(x)
           
        
if __name__ == "__main__":
    Try.main(sys.argv)

Try-Except

In Python, we use a Try-Except construct to detect and handle exceptions in our code. Let’s look at a quick example, and then we can discuss how it works.

try:
  if len(args) > 1:
    reader = open(args[1])
  else:
    reader = sys.stdin
  x = int(reader.readline())
  print(x)
    
except Exception as e:
  print("Error!")

First, we use the try keyword, followed by a colon : to begin a block of code. Below that, we indent any lines to be included in the try block. If any exceptions occur in the code indented below a try keyword, we can add except statements to handle that exception.

Directly following the try block, we must include at least one except statement. The except keyword is followed by the name of an exception that we’d like to catch. Optionally we can add the as keyword and the name of a variable to represent that exception. Finally, we include a colon : and indent a block of code below to act as the exception handler.

In this example, we are just catching the generic Exception type, which will match any catchable exception. We’ll see how we can detect specific exceptions in a later example. We can use the variable for the exception, in this case e, to access additional details about the exception we’ve detected.

Note: nearly every other language uses the syntax "try catch" instead of "try except".

Which Exceptions to Catch?

When writing code, it can sometimes be very difficult to even know which exceptions to expect from a particular piece of code. Thankfully, the Python Documentation includes quite a bit of information about the possible exceptions that could occur in a program.

Let’s return to our earlier example. Here is the code contained in the try block:

if len(args) > 1:
  reader = open(args[1])  
else:
  reader = sys.stdin

x = int(reader.readline())
print(x)

While this may look like a very simple few lines of code, there are actually several exceptions that could be produced here:

  • FileNotFoundError - if the file in args[1] is not valid
  • NameError - if we forget to include import sys at the top of the file
  • ValueError - if the input cannot be converted to an integer
  • IndexError - args[1] could produce this exception, but we’ve already checked that the length is greater than 1, so it won’t happen in practice.

When writing truly bulletproof code, it is a good idea to attempt to catch and handle all of these exceptions if possible. You can always refer to the official Python documentation for a list of exceptions that could occur. Unfortunately, Python does not do a great job of documenting which exceptions could be thrown by specific methods compared to Java. Later on in this chapter we’ll discuss some best practices when it comes to detecting and handling exceptions in code.

Handling Multiple Exceptions

Of course, we can add multiple except statements after any try block to catch different types of exceptions.

try:
  # If an argument is present, we are reading from a file
  # specified in args[1]
  if len(args) > 1:
    reader = open(args[1])

  # If no argument, read from stdin  
  else:
    reader = sys.stdin

  x = int(reader.readline())
  print(x)

except FileNotFoundError as e:
  print("Error: File Not Found!")
except ValueError as e:
  print("Error: Input Does Not Match Expected Format!")
except OSError as e:
  print("Error: OS Exception!")

In the example code above, we see three different except statements, each of which will handle a different type of exception. When an exception occurs in the code contained in a try block, the Python interpreter will create the exception, and then it will search for the first handler that matches. So, it will begin with the first except statement, and see if the exception created matches the type of the exception in parenthesis. If so, it will execute the code inside of that except block. If not, it will continue to the next except statement. If none of the except statements match the exception, then it will be thrown and cause the program to stop executing.

Exception Hierarchy

The exceptions in Python form a hierarchical structure, meaning that an exception may match multiple types. For example, all exceptions that programs can catch are based on the generic Exception type. So, FileNotFoundError and IndexError would both match the Exception type.

In addition, many exceptions are descended from the OSError type. For example, FileNotFoundError is based on OSError, which itself is based on the Exception type. So, a FileNotFoundError would match any of those three types of exceptions. It can make things a bit tricky!

So, how can we know what types of exceptions we are dealing with, and what the hierarchy is? Thankfully, the Python Documentation contains information about most possible exceptions, including the entire hierarchy of those exceptions.

For example, here is a screenshot from the Python Documentation page showing the hierarchy of OSError:

OSError Hierarchy OSError Hierarchy

Because of this, we must be careful about how we order the catch statements. In general, we want to place the more specific exceptions first (the ones further down the hierarchy), and the more generic exceptions later.

Let’s look at an example of poor ordering of exception handlers:

try:
  # If an argument is present, we are reading from a file
  # specified in args[1]
  if len(args) > 1:
    reader = open(args[1])

  # If no argument, read from stdin  
  else:
    reader = sys.stdin

  x = int(reader.readline())
  print(x)

except OSError as e:
  print("Error: OS Exception!")
except FileNotFoundError as e:
  print("Error: File Not Found!")
except ValueError as e:
  print("Error: Input Does Not Match Expected Format!")

In the code above, we are catching the OSError first. So, if the code produces a FileNotFoundError, it will be caught and handled by the first except statement, since FileNotFoundError is also an OSError. Therefore, we wouldn’t want to order our except statements in this way.

Try It!

Let’s see if we can write a very simple program to catch and handle a few common exceptions. Here’s some starter code for main’s:

if len(args) > 1:
  reader = open(args[1])
else:
  reader = sys.stdin

x = int(reader.readline())
y = int(reader.readline())
z = x / y

print(z)

For this example, place this starter code in Try.py, open to the left, and modify it to catch and handle the following exceptions by printing the given error messages:

  • FileNotFoundError - print “Error: File Not Found!”
  • ValueError - print “Error: Input Does Not Match Expected Format!”
  • ZeroDivisionError - print “Error: Divide by Zero!”

Any other exceptions can be ignored.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Subsections of Try-Except

Raise

YouTube Video

Video Materials

Beyond catching exceptions, there are a few other important concepts to learn related to exceptions. Let’s go over a couple of them.

Raise

We can generate our own exceptions using the raise keyword. This allows us to create new exceptions whenever needed. Let’s look at an example:

import sys

class Raise:
    
    @classmethod
    def main(cls, args):       
        if len(args) > 1:
          reader = open(args[1])
        else:
          reader = sys.stdin
        x = float(reader.readline())
        y = float(reader.readline())
        if y == 0:
          raise ValueError("Cannot divide by zero")
        else:
          z = x / y
          print(z)

In this code, we are asking the user to input two floating point numbers. Then, we will output the first number divided by the second. However, we want to avoid the situation where the second number, the divisor, is $0$. Instead of causing a DivideByZeroError, we can use an If-Then statement and the raise keyword to send a ValueError instead.

Following the raise keyword, we must create a new Exception. In this case, we are creating a new ValueError, but any of the exception types we’ve learned about so far will work the same way. Inside of the parentheses, we can provide a helpful error message to go along with this exception.

Try It!

Let’s see if we can use these keywords in another example. We’ll start with this code in main():

if len(args) > 1:
  reader = open(args[1])
else:
  reader = sys.stdin

Place this code in Raise.py and modify it to do the following:

  1. If the user provides an invalid file name in the arguments array, it should throw a FileNotFoundError.
  2. It should read a single string of input from the user. To make this simple, don’t forget to use the strip() method to remove any extra whitespace and trailing newline characters!
  3. If the string does not begin with a capital letter, it should throw a new ValueError that contains the error message “Proper Capitalization Required!”
  4. Of course, if the string is empty, it should throw an IndexError with the message “String is Empty!” when it tries to access the first character of the string.
  5. Otherwise, it should print the string to the terminal.
Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Passing Back Text

The construct raise <exception>(<string>) returns an <class:ValueError> object. Like all objects it has a __str__ method that is called when printed or used with str(). The value returned is by str() is <string>.

try:
    raise ValueError("demo")
except ValueError as e:
   print(type(e)) # <class 'ValueError'>
   print(e)       # "demo"

This behavior is convenient when a single type of exception can be triggered by different causes. Each raise .. can have a different explanatory string or error message.

Subsections of Raise

Else & Finally

YouTube Video

Video Materials

When dealing with exceptions in our code, sometimes we have an operation that must be executed, even if the earlier code throws an exception. In that case, we can use the finally keyword to make sure that the correct code is executed. In addition, we may have an operation that should only happen if there are no exceptions in the previous code. For that, we can use the else keyword.

Finally

To understand how the finally keyword works, let’s take a look at an example:

import sys

reader = sys.stdin

try:
  x = int(reader.readline())
  if x < 0:
    raise ValueError("Input must be greater than 0!")
except ValueError as e:
  print("Value Error: {}".format(e))
finally:
  print("Finally Block")

print("After Try")

This program will read an integer from the terminal. If the integer is greater than or equal to 0, it will do nothing except print the “Finally Block” and “After Try” messages. However, if the input is less than 0, it will throw a ValueError. Finally, if the input is not a number, then a ValueError will also be thrown and handled. In each of those cases, it will also print the “Finally Block” message.

Let’s run this program a few times and see how it works. First, we’ll provide the input “5”:

$ python3 8p-except/Finally.py
5
Finally Block
After Try

Here, we can see that the code in the finally block always runs when the program is finished executing the statements in the try block.

Let’s run it again, this time with “-5” as the input:

$ python3 8p-except/Finally.py
-5
Value Error: Input must be greater than 0!
Finally Block
After Try

Here, we can see that it prints the error message caused by the ValueError, then proceeds to the finally block. So, even if an exception is thrown while inside of the try block, the code in the finally block is always executed once the try block and any exception handlers are finished.

Here’s one more example, this time with “abc” as the input:

$ python3 8p-except/Finally.py
abc
Value Error: invalid literal for int() with base 10: 'abc\n'
Finally Block
After Try

Once again, we see that the code first handles the ValueError, and then it will execute the code in the finally block.

In a later chapter, we’ll learn more about how to use the finally block to perform important tasks such as closing open files and making sure we don’t leave the system in an unstable state when we handle an exception.

Else

Python also allows the else keyword to be used with a Try-Except statement. It works similarly to the way Python handles the else keyword with loops. Let’s look at an example:

import sys

reader = sys.stdin

try:
  x = int(reader.readline())
  if x < 0:
    raise ValueError("Input must be greater than 0!")
except ValueError as e:
  print("Value Error: {}".format(e))
else:
  print("No Errors!")
finally:
  print("Finally Block")

print("After Try")

The code above is the same example used to demonstrate the finally keyword, but now there is an else block added. The code in the else block will be executed only if the entire code in the try block is able to execute without any errors or exceptions being raised.

So, if we run this program with the input 5, we’ll get the following:

$ python3 8p-except/Finally.py
5
No Errors!
Finally Block
After Try

Since this program does not encounter any exceptions, the else block is executed immediately after the try block, but before the finally block. However, if we provide an invalid input, such as -5, the output will look like this instead:

$ python3 8p-except/Finally.py
-5
Value Error: Input must be greater than 0!
Finally Block
After Try

Since this program encountered an exception, the code in the else block is not executed. However, the code in the finally block is still executed, since it will happen regardless of the outcome of the try block.

Finally, it is possible to include an else block without a finally block if desired. The only rule is that the else block must come before the finally block, and both of them should be after any except blocks to handle exceptions.

Subsections of Else & Finally

With

Lastly, Python includes another new statement that is relevant to dealing with exceptions, called the With statement.

With

To understand how the With statement works in Python, let’s look at an example:

import sys
import traceback
import io

try:
    if len(sys.argv) >1:
        with open(sys.argv[1]) as input_file:
            data = "".join(input_file.readlines())
            reader = io.StringIO(data)
    else:
        reader = sys.stdin 
    
    x = int(reader.readline())
    print(x)

  
except ValueError as e:
  print("ValueError: {}".format(e))
except FileNotFoundError as e:
  print("FileNotFoundError: {}".format(e))

In this example, we are first using the standard If statement to either open a file provided as the first command-line argument, or else we are reading input from the terminal.

If there is a file provided, we open it using a with statement. This statement is used to create a context manager in Python. In short, it allows languages developers to define things that must always be done before and after a resource, such as an open file, is used. In this example, in addition to opening the file, it will make sure the infile is closed when we leave the with block. The other statements manipulate the file into StringIO object that uses the same methods as sys.stdin.

When the code in the with statement throws an exception, Python will automatically perform the cleanup operations for the resource listed in the with. So, when we are reading input from a file, it would close that file and make sure that it isn’t damaged or left open when our program crashes.

In addition, any additional exceptions thrown when trying to close the file are suppressed by the system, so we only see the exception that caused the initial error. This is much better than using a finally statement to close the file, since we’d have to run the risk of throwing a second exception inside of the finally statement.

This type of statement is great when writing programs that will handle large amounts of input, either by connecting to a database, reading from a file, or using the Internet to communicate with a server. Many standard Python libraries can be used in this way, especially if they require a close() or other cleanup method to be called when we are finished using them. So, a with statement is a great way to make sure those programs are able to handle exceptions without leaving the system in an unstable state.

WARNING: avoid using **with** in conjunction with stdin.
`with sys.stdin as reader:` is bad. Python will dutifully close the stdin at the end of the with-block -- making it unavailable to the program a second time. This would be like having to close and reopen Word, because every time you hit `return` the keyboard stopped working.

More on with ... as ...: after we cover File input.

Subsections of With

Best Practices

Now that we’ve seen lots of information about how to throw, catch, and handle exceptions in Python, it is a great time to discuss some of the best practices we can apply in our code to make it as readable and bulletproof as possible. While these aren’t strict rules that must be followed all the time, they are great things to keep in mind as we write code that deals with exceptions.

Leave No Exception Unhandled

As much as possible, we should write our code to handle any exception we can reasonably expect our users to run into when using our programs. We cannot assume that users will always provide the correct input in the correct format, and a single typo by a user should not cause our entire program to crash.

Instead, we should always use Try-Except statements whenever possible to detect and handle those errors as simply as possible. In addition, if the program is interactive, we can combine that approach with the use of loops to prompt the user to provide new input to resolve the error.

Don’t Substitute Unchecked Exception Handling for Value Checking

You can go “try except” crazy. Exception handling is powerful, but also slow, really really slow.

You can write something like

try:
    ratio = x / y
except ZeroDivisionError as e:
    print("Cannot divide by zero")

but

if y !=0:
    ratio = x / y;
else:
    print("Cannot divide by zero")

executes a lot more efficiently.

Many exceptions can be avoided through value checking. In this course, we may direct you to throw and catch exceptions of this type for practice in exception coding.

The Python Documentation is an okay resource to determine which exceptions could be thrown by any methods used in our code. However Python does not provide a standard or comprehensive list of the Exceptions and conditions which cause them for all methods in all modules–nor is their enumeration in the Python Docs in a standard place or format.

Dp Not Use Exception Handling for Control Flow

You might be tempted to do something like this

try:
    reader = open(sys.argv[1])
except Exception:
    reader = sys.stdin 

Where if there is any problem accessing sys.argv[1] as a file, such as it was not provided or it does not exist, you just use the keyboard. Here you are letting an exception control what is happening without telling the user there was a problem. Better would be

if len(sys.argv) > 1:
    try:
        reader= open(sys.arv[1])
    except OSError:
        print ("there was a problem accessing {}, please check the file and try again".format(sys.argv[1]))
        return # here we assume this will exit the program
else:
    reader = sys.stdin

Programmers do not expect control flow decisions to be made in exception-handlers. It makes analysis of the program very difficult.

Be Specific

We should also always strive to use specific exceptions in our except statements whenever we can. So, if we are opening a file, we should include an except statement for a FileNotFoundError, and not just a single generic Exception. Even though this means we may have to include several except statements, it is much better in the long run because it allows us to know exactly what happened when our code has a problem.

In addition, we should always make sure we catch the most specific exceptions first, before any more generic exceptions. As we saw earlier, if we try to catch an OSError before a FileNotFoundError, we won’t be able to tell when we’ve reached a FileNotFoundError at all, since it is a subtype of the OSError.

Use Messages and Stack Traces

The Python Exception class includes some very helpful methods we can use to get additional information when we encounter an exception.

Here’s an example to show what we can learn:

import sys
import traceback

try:
  x = 5 / 0
except ZeroDivisionError as e:
  print("Error! Trying to Divide by Zero!")
  traceback.print_exc(file=sys.stdout)

When this code is executed, it will produce the following output:

$ python3 8p-except/StackTrace.py
Error! Trying to Divide by Zero!
Traceback (most recent call last):
  File "8p-except/StackTrace.py", line 5, in <module>
    x = 5 / 0
ZeroDivisionError: division by zero

On the first line, we are printing our own error message. Below that, we are using traceback.print_exc(file=sys.stdout) to print a full stack trace showing the location of the error in code. In this example, we included file=sys.stdout because the method will print to sys.stderr by default, a different output stream. This just makes it easier to see all of our output on the terminal for now.

In general, it is best to only show a short error message to users, but in some cases it may be better to add our own message. The text “division by zero” isn’t very clear, even to developers. Instead, we could say “Error: divisor cannot be zero” to make the error message clearer.

The information contained in the stack trace is very helpful to developers when trying to debug and fix problems in the code, but that information is very confusing to users. As we learn how to build more advanced programs, we’ll see how to record and log those error messages and stack traces for debugging, but hide them from our users.

To see what other methods are available, refer to the Traceback library in the Python documentation.

Don’t Ignore Exceptions

Another big problem is that many developers tend to catch exceptions, only to ignore them so their program doesn’t crash. Here’s a quick example:

import sys

x = 0
reader = sys.stdin
try:
  x = int(reader.readline())
except Exception as e:
  pass
  # do nothing

In this code, if the user inputs something that can’t be converted to an integer, the code just silently ignores the exception and proceeds with x still storing the value $0$. While that may not cause issues, it can also make it very frustrating to debug later issues or changes in this program. So, it is always best to output an error message when an exception occurs, even if it can be easily ignored without additional input from the user. This will make it easier to debug additional issues later on in development.

Bulletproof Code

Here’s a simple program that asks the user for input, and will repeat the question until a valid input is received. This code is designed to handle many common situations and exceptions:

import sys
import traceback

x = 0

try:
  with open(sys.argv[1]) as reader:
    while x <= 0:
      try:
        x = int(reader.readline())
        if x <= 0:
          print("Error: Negative Integer Detected!")
      except ValueError as e:
        print("Error: Integer Not Found!")
      except Exception as e:
        print("Error: Unknown Input Error")
        sys.exit() # probably can't recover, so stop executing

  print("the file contained {}".format(x))

except FileNotFoundError as e:
  print("Error: Unable to File Open Reader!")
except Exception as e:
  print("Error: Unable to File Open Reader!")

There are several items in this code that help make it very bulletproof:

  1. First, the program uses a Try-Except statement containing a With statement to make sure that the file will be properly closed when we are done with it. As we’ve discussed, this is a good practice when handling input via files, but really any input object can be handled in this way.
  2. The outer Try-Except statement has a two except blocks. The first handles not finding the file (the most likely exception). The second captures anything else that might go wrong with the reader .
  3. Inside of the While loop, there is a second Try-Except statement to handle errors that come from reading an individual input, such as a ValueError.
  4. In addition, that inner Try-Except statement will also catch any generic Exceptions that might occur when reading input. In that case, we use the sys.exit() method to stop executing the program, since it may be difficult to continue to read input in that case. Thankfully, since we are using a With block, the reader will be correctly closed for us automatically. In addition, any finally blocks would be executed before the program stops.
Don’t Exit Early!

We have not discussed the sys.exit() method yet, but you may have come across it while reading code online. In Python, the sys.exit() method is used to exit the Python interpreter safely from within a program

For now, if we find that we need to exit our program when handling an error, we can safely use the sys.exit() method as shown in the example above. It will make sure that any finally blocks and With statements are executed properly.

There are some other ways to exit the Python interpreter that you can find online, but they may do so without properly closing any resources or executing finally blocks. They can also make your code much more difficult to maintain and test. This can be very dangerous!

Of course, there are many things that can be done differently in this code, depending on our preferences and how we’d like our program to function. This is just one possible way to build useful code that handles several exceptions.

A Worked Example

Let’s work through an entire example program together to see how we can build programs that handle exceptions quickly and easily, without allowing the program to crash when invalid input is received.

Problem Statement

Consider the following problem statement for a driver-class program Example.py:

Write a program to accept a list of numbers, one per line from the terminal.

The numbers can either be whole numbers or floating point numbers. The program should continue to accept input until a number equal to $0$ is input, or a blank line is received. Of course, if the input in a file ends before a $0$, that can be treated as the end of the input.

Once $0$ is received or the input ends, the program should print the largest and smallest number provided as input (not including the $0$ to end input). Sample outputs are shown below.

The program should catch and handle all common exceptions. When an exception is caught, the program should print the name of the exception, followed by the message included in the exception. It should then continue to accept input (if possible).

Sample Inputs & Outputs

Here’s an example of an expected input for the program:

5.0
3
8.5
7
k
2.3.6
0.0

Here is the correct output for that input:

ValueError: could not convert string to float: 'k'
ValueError: could not convert string to float: '2.3.6'
Maximum: 8.5
Minimum: 3.0

Pause a moment to think about control flow, loops and exceptions you might need to handle.

When first starting out in coding and exception handling, it is often best to first write the code without exception handling, test it, then add the exception stuff. Exception handlers can hide logic errors.

Handle Input

First, we can write code to handle user input. The problem statement tells us to read inputs, one per line, until the user inputs 0. We also know that the user can input either whole numbers or floating point numbers. So, to make it simple, we’ll just use floating point numbers for everything– this will require us to import math for number comparisons. Now, math.isclose() cannot handle None, so instead we’ll need to pick an initial value for our loop condition variable that will allow us to enter the loop, i.e. make the loop condition True.

x = 1.0  # math.isclose() cannot handle None, wo instead we pick an initial value for x 
while not math.isclose(x,0.0):
  string = reader.readline()
  if len(string) == 0: 
    x = 0.0
  else:
    x = float(string)

If there is a a blank line, the strip() method will return the empty string. Our program should take the the same action as if seeing a 0.0 so we assign 0.0 to x. You should at this point test your logic , then add exception handlers. Here we know that if string cannot convert until we’ll get a ValueError.

x = 1.0
while not math.isclose(x, 0.0):
    try:
        string = reader.readline().strip()
        if len(string) == 0:
            x = 0.0
        else:
            x = float(string)
            if not math.isclose(x, 0.0):  
                if x < minimum or math.isclose(minimum, 0.0):
                    minimum = x
                if x > maximum or math.isclose(maximum, 0.0):
                    maximum = x
    except ValueError as e:
        print("ValueError: {}".format(e))

This should handle most exceptions that will occur in this program. Of course, there are many other ways that this code could be structured as well.

Logic

Once we have the code to handle input, we can write the code to deal with logic. Keeping track of the minimum and maximum of a set of inputs is pretty simple:

maximum = 0.0
minimum = 0.0
x = 1.0
while not math.isclose(x, 0.0):
    try:
        string = reader.readline().strip()
        if len(string) == 0:
            x = 0.0
        else:
            x = float(string)
            if not math.isclose(x, 0.0):  
                if x < minimum or math.isclose(minimum, 0.0):
                    minimum = x
                if x > maximum or math.isclose(maximum, 0.0):
                    maximum = x
    except ValueError as e:
        print("ValueError: {}".format(e))

    
print("Maximum: {}".format(maximum))
print("Minimum: {}".format(minimum))

We can simply create variables to track the minimum and maximum values. Since our program won’t accept $0$ as an input, we can use that as the default value for these variables. Our logic does not use any functions likely to throw new errors, so no more exception handling is required.

Testing

Finally, we may want to do some quick testing of our program. In theory, it should handle any inputs provided, including files that don’t exist as command-line arguments and more. When testing, we should always see if we can find some input that breaks our program, then adjust the program as needed to prevent that problem. Of course, we should always make sure that it produces the correct answers, too.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Subsections of A Worked Example