# 13. Exceptions ## 13.1. What is an exception? An *exception* is a signal that a condition has occurred that can’t be easily handled using the normal flow-of-control of a Python program. *Exceptions* are often defined as being “errors” but this is not always the case. All errors in Python are dealt with using *exceptions*, but not all *exceptions* are errors. ## 13.2. Exception Handling Flow-of-control To explain what an *exception* does, let’s review the normal “flow of control” in a Python program. In normal operation Python executes statements sequentially, one after the other. For three constructs, if-statements, loops and function invocations, this sequential execution is interrupted. * For *if-statements*, only one of several statement blocks is executed and then flow-of-control jumps to the first statement after the if-statement. * For *loops*, when the end of the loop is reached, flow-of-control jumps back to the start of the loop and a test is used to determine if the loop needs to execute again. If the loop is finished, flow-of-control jumps to the first statement after the loop. * For *function invocations*, flow-of-control jumps to the first statement in the called function, the function is executed, and the flow-of-control jumps back to the next statement after the function call. Do you see the pattern? If the flow-of-control is not purely sequential, it always executes the first statement immediately following the altered flow-of-control. That is why we can say that Python flow-of-control is sequential. ### 13.2.1. Raising and Catching Errors with `try` and `except` The try/except control structure provides a way to process a run-time error and continue on with program execution. Until now, any run-time error, such asking for the 8th item in a list with only 3 items, or dividing by 0, has caused the program execution to stop. In the browser ActiveCode windows, you get an error message in a box below. When you are executing python programs from the command-line, you also get an error message saying something about what went wrong and what line it occurred on. After the run-time error is encountered, the python interpreter does not try to execute the rest of the code. You have to make some change in your code and rerun the whole program. With try/except, you tell the python interpreter: * Try to execute a block of code, the “try” clause. : + If the whole block of code executes without any run-time errors, just carry on with the rest of the program after the try/except statement. * If a run-time error does occur during execution of the block of code: : + skip the rest of that block of code (but don’t exit the whole program) + execute a block of code in the “except” clause + then carry on with the rest of the program after the try/except statement ```python try:
except
:
``` The syntax is fairly straightforward. The only tricky part is that after the word except, there can optionally be a specification of the kinds of errors that will be handled. The catchall is the class Exception. If you write `except Exception:` all runtime errors will be handled. If you specify a more restricted class of errors, only those errors will be handled; any other kind of error will still cause the program to stop running and an error message to be printed. The code below causes an error of type IndexError, by trying to access the third element of a two-element list. ```python items = ['a', 'b'] third = items[2] ``` The code below causes an error of type ZeroDivisionError, or less specifically ArithmeticError. ```python x = 5 y = x/0 ``` Let’s see what happens if we wrap some of this problematic code in a try/except statement. Note that `this won't print` doesn’t print: when the error is encountered, the rest of the try block is skipped and the exception block is executed. When the except block is done, it continues on with the next line of code that’s outdented to the same level as the try: `continuing` is printed. ```python try: items = ['a', 'b'] third = items[2] print("This won't print") except Exception: print("got an error") print("continuing") ``` If we catch only IndexEror, and we actually have a divide by zero error, the program does stop executing. ```python try: items = ['a', 'b'] third = items[2] print("This won't print") except IndexError: print("error 1") print("continuing") try: x = 5 y = x/0 print("This won't print, either") except IndexError: print("error 2") print("continuing again") ``` There’s one other useful feature. The exception code can access a variable that contains information about exactly what the error was. Thus, for example, in the except clause you could print out the information that would normally be printed as an error message but continue on with execution of the rest of the program. To do that, you specify a variable name after the exception class that’s being handled. The exception clause code can refer to that variable name. ```python try: items = ['a', 'b'] third = items[2] print("This won't print") except Exception as e: print("got an error") print(e) print("continuing") ``` ## 13.3. Runetime Stack and `raise` command There are cases where the sequential flow-of-control does not work well. An example will best explain this. Let’s suppose that a program contains complex logic that is appropriately subdivided into functions. The program is running and it currently is executing function D, which was called by function C, which was called by function B, which was called by function A, which was called from the main function. This is illustrated by the following simplistic code example: ```python def main() A() def A(): B() def B(): C() def C(): D() def D() # processing ``` Function D determines that the current processing won’t work for some reason and needs to send a message to the main function to try something different. However, all that function D can do using normal flow-of-control is to return a value to function C. So function D returns a special value to function C that means “try something else”. Function C has to recognize this value, quit its processing, and return the special value to function B. And so forth and so on. It would be very helpful if function D could communicate directly with the main function (or functions A and B) without sending a special value through the intermediate calling functions. Well, that is exactly what an *exception* does. An *exception* is a message to any function currently on the executing program’s “run-time-stack”. (The “run-time-stack” is what keeps track of the active function calls while a program is executing.) In Python, your create an *exception* message using the `raise` command. The simplest format for a `raise` command is the keyword `raise` followed by the name of an exception. For example: ```python raise ExceptionName ``` So what happens to an *exception* message after it is created? The normal flow-of-control of a Python program is interrupted and Python starts looking for any code in its run-time-stack that is interested in dealing with the message. It always searches from its current location at the bottom of the run-time-stack, up the stack, in the order the functions were originally called. A `try: except:` block is used to say “hey, I can deal with that message.” The first `try: except:` block that Python finds on its search back up the run-time-stack will be executed. If there is no `try: except:` block found, the program “crashes” and prints its run-time-stack to the console. Let’s take a look at several code examples to illustrate this process. If function D had a `try: except:` block around the code that `raised` a `MyException` message, then the flow-of-control would be passed to the local `except` block. That is, function D would handle it’s own issues. ```python def main() A() def A(): B() def B(): C() def C(): D() def D() try: # processing code if something_special_happened: raise MyException except MyException: # execute if the MyException message happened ``` But perhaps function C is better able to handle the issue, so you could put the `try: except:` block in function C: ```python def main() A() def A(): B() def B(): C() def C(): try: D() except MyException: # execute if the MyException message happened def D() # processing code if something_special_happened: raise MyException ``` But perhaps the main function is better able to handle the issue, so you could put the `try: except:` block in the main function: ```python def main() try: A() except MyException: # execute if the MyException message happened def A(): B() def B(): C() def C(): D() def D() # processing code if something_special_happened: raise MyException ``` ## 13.4. Summary Let’s summarize our discussion. An *exception* is a message that something “out-of-the-ordinary” has happened and the normal flow-of-control needs to be abandoned. When an *exception* is `raised`, Python searches its run-time-stack for a `try: except:` block that can appropriately deal with the condition. The first `try: except:` block that knows how to deal with the issue is executed and then flow-of-control is returned to its normal sequential execution. If no appropriate `try: except:` block is found, the program “crashes” and prints its run-time-stack to the console. As our final example, here is a program that crashes because no valid `try: except:` block was found to process the `MyException` message. Notice that the `try: except:` block in the main function only knows how to deal with `ZeroDivisonError` messages, not `MyException` messages. ```python def main() try: A() except ZeroDivisonError: # execute if a ZeroDivisonError message happened def A(): B() def B(): C() def C(): D() def D() # processing code if something_special_happened: raise MyException ``` ## 13.5. Standard Exceptions Most of the standard *exceptions* built into Python are listed below. They are organized into related groups based on the types of issues they deal with. | Language Exceptions | Description | | --- | --- | | StandardError | Base class for all built-in exceptions except StopIteration and SystemExit. | | ImportError | Raised when an import statement fails. | | SyntaxError | Raised when there is an error in Python syntax. | | IndentationError | Raised when indentation is not specified properly. | | NameError | Raised when an identifier is not found in the local or global namespace. | | UnboundLocalError | Raised when trying to access a local variable in a function or method but no value has been assigned to it. | | TypeError | Raised when an operation or function is attempted that is invalid for the specified data type. | | LookupError | Base class for all lookup errors. | | IndexError | Raised when an index is not found in a sequence. | | KeyError | Raised when the specified key is not found in the dictionary. | | ValueError | Raised when the built-in function for a data type has the valid type of arguments, but the arguments have invalid values specified. | | RuntimeError | Raised when a generated error does not fall into any category. | | MemoryError | Raised when a operation runs out of memory. | | RecursionError | Raised when the maximum recursion depth has been exceeded. | | SystemError | Raised when the interpreter finds an internal problem, but when this error is encountered the Python interpreter does not exit. | | Math Exceptions | Description | | --- | --- | | ArithmeticError | Base class for all errors that occur for numeric calculation. You know a math error occurred, but you don’t know the specific error. | | OverflowError | Raised when a calculation exceeds maximum limit for a numeric type. | | FloatingPointError | Raised when a floating point calculation fails. | | ZeroDivisonError | Raised when division or modulo by zero takes place for all numeric types. | | I/O Exceptions | Description | | --- | --- | | FileNotFoundError | Raised when a file or directory is requested but doesn’t exist. | | IOError | Raised when an input/ output operation fails, such as the print statement or the open() function when trying to open a file that does not exist. Also raised for operating system-related errors. | | PermissionError | Raised when trying to run an operation without the adequate access rights. | | EOFError | Raised when there is no input from either the raw\_input() or input() function and the end of file is reached. | | KeyboardInterrupt | Raised when the user interrupts program execution, usually by pressing Ctrl+c. | | Other Exceptions | Description | | --- | --- | | Exception | Base class for all exceptions. This catches most exception messages. | | StopIteration | Raised when the next() method of an iterator does not point to any object. | | AssertionError | Raised in case of failure of the Assert statement. | | SystemExit | Raised when Python interpreter is quit by using the sys.exit() function. If not handled in the code, it causes the interpreter to exit. | | OSError | Raises for operating system related errors. | | EnvironmentError | Base class for all exceptions that occur outside the Python environment. | | AttributeError | Raised in case of failure of an attribute reference or assignment. | | NotImplementedError | Raised when an abstract method that needs to be implemented in an inherited class is not actually implemented. | All exceptions are objects. The classes that define the objects are organized in a hierarchy, which is shown below. This is important because the parent class of a set of related exceptions will catch all exception messages for itself and its child exceptions. For example, an `ArithmeticError` exception will catch itself and all `FloatingPointError`, `OverflowError`, and `ZeroDivisionError` exceptions. ```python BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration +-- StopAsyncIteration +-- ArithmeticError | +-- FloatingPointError | +-- OverflowError | +-- ZeroDivisionError +-- AssertionError +-- AttributeError +-- BufferError +-- EOFError +-- ImportError +-- LookupError | +-- IndexError | +-- KeyError +-- MemoryError +-- NameError | +-- UnboundLocalError +-- OSError | +-- BlockingIOError | +-- ChildProcessError | +-- ConnectionError | | +-- BrokenPipeError | | +-- ConnectionAbortedError | | +-- ConnectionRefusedError | | +-- ConnectionResetError | +-- FileExistsError | +-- FileNotFoundError | +-- InterruptedError | +-- IsADirectoryError | +-- NotADirectoryError | +-- PermissionError | +-- ProcessLookupError | +-- TimeoutError +-- ReferenceError +-- RuntimeError | +-- NotImplementedError | +-- RecursionError +-- SyntaxError | +-- IndentationError | +-- TabError +-- SystemError +-- TypeError +-- ValueError | +-- UnicodeError | +-- UnicodeDecodeError | +-- UnicodeEncodeError | +-- UnicodeTranslateError +-- Warning +-- DeprecationWarning +-- PendingDeprecationWarning +-- RuntimeWarning +-- SyntaxWarning +-- UserWarning +-- FutureWarning +-- ImportWarning +-- UnicodeWarning +-- BytesWarning +-- ResourceWarning ``` ## 13.6. Principles for using Exceptions There are many bad examples of *exception* use on the Internet. The purpose of an *exception* is to modify the flow-of-control, not to catch simple errors. If your `try: except:` block is in the same function that `raises` the exception, you are probably mis-using exceptions. Principle 1: If a condition can be handled using the normal flow-of-control, don’t use an exception! Example 1: | | | | --- | --- | | **DON’T DO THIS**: | When you can just as easily test for no items in the list doing this: | | ```python try: average = sum(a_list) / len(a_list) except ZeroDivisionError: average = 0 ``` | ```python if len(a_list) > 0: average = sum(a_list) / len(a_list) else: average = 0 ``` | Example 2: | | | | --- | --- | | **DON’T DO THIS**: | When you can just as easily test for a valid index doing this: | | ```python try: value = my_list[index] except IndexError: value = -1 ``` | ```python if 0 <= index < len(my_list): value = my_list[index] else: value = -1 ``` | Example 3: | | | | --- | --- | | **DON’T DO THIS**: | When you can just as easily test to see if the key is valid doing this: | | ```python try: value = my_dictionary[key] except KeyError: value = -1 ``` | ```python if key in my_dictionary.keys(): value = my_dictionary[key] else: value = -1 ``` | Principle 2: If you call a function that potentially raises exceptions, and you can do something appropriate to deal with the exception, then surround the code that contains the function call with a `try: except:` block. Example: Suppose you have a function that reads a file to set the state of an application when it starts up. You should catch any errors related to reading the file and set the state of the application to default values if they can’t be set from the file. ```python try: load_state('previous_state.txt') except OSError: set_state_to_defaults() ``` Principle 3: If you call a function that potentially raises exceptions, and you can’t do anything meaningful about the conditions that are raised, then don’t catch the exception message(s). ## 13.7. Exceptions Syntax There are many variations on the code that catches exceptions. Here is a brief summary, but other code variations are possible. ### 13.7.1. Catch All Exceptions Catch all exceptions, regardless of their type. This will prevent your program from crashing, but this type of exception handling is rarely useful because you can’t do anything meaningful to recover from the abnormal condition. ```python try: # Your normal code goes here. # Your code should include function calls which might raise exceptions. except: # If any exception was raised, then execute this code block. ``` ### 13.7.2. Catch A Specific Exception This is perhaps the most often used syntax. It catches one specific condition and tries to re-cover from the condition. ```python try: # Your normal code goes here. # Your code should include function calls which might raise exceptions. except ExceptionName: # If ExceptionName was raised, then execute this block. ``` ### 13.7.3. Catch Multiple Specific Exceptions ```python try: # Your normal code goes here. # Your code should include function calls which might raise exceptions. except Exception_one: # If Exception_one was raised, then execute this block. except Exception_two: # If Exception_two was raised, then execute this block. else: # If there was no exception then execute this block. ``` ### 13.7.4. Clean-up After Exceptions If you have code that you want to be executed even if exceptions occur, you can include a `finally` code block: ```python try: # Your normal code goes here. # Your code might include function calls which might raise exceptions. # If an exception is raised, some of these statements might not be executed. finally: # This block of code will always execute, even if there are exceptions raised ``` ### 13.7.5. An Example of File I/O One place where you will always want to include exception handling is when you read or write to a file. Here is a typical example of file processing. Note that the outer `try: except:` block takes care of a missing file or the fact that the existing file can’t be opened for writing. The inner `try: except:` block protects against output errors, such as trying to write to a device that is full. The `finally` code guarantees that the file is closed properly, even if there are errors during writing. ```python try: f = open("my_file.txt", "w") try: f.write("Writing some data to the file") finally: f.close() except IOError: print "Error: my_file.txt does not exist or it can't be opened for output." ``` ## 13.8. The `finally` clause of the `try` statement A common programming pattern is to grab a resource of some kind — e.g. we create a window for turtles to draw on, or we dial up a connection to our internet service provider, or we may open a file for writing. Then we perform some computation which may raise an exception, or may work without any problems. Whatever happens, we want to “clean up” the resources we grabbed — e.g. close the window, disconnect our dial-up connection, or close the file. The `finally` clause of the `try` statement is the way to do just this. Consider this (somewhat contrived) example: ```python import turtle import time def show_poly(): try: win = turtle.Screen() # Grab/create a resource, e.g. a window tess = turtle.Turtle() # This dialog could be cancelled, # or the conversion to int might fail, or n might be zero. n = int(input("How many sides do you want in your polygon?")) angle = 360 / n for i in range(n): # Draw the polygon tess.forward(35) tess.left(angle) time.sleep(3) # Make program wait a few seconds except Exception as e: print("insufficient number of sides") print(e) finally: win.bye() # Close the turtle's window show_poly() ``` In lines 20–22, `show_poly` is called three times. Each one creates a new window for its turtle, and draws a polygon with the number of sides input by the user. But what if the user enters a string that cannot be converted to an `int`? What if they close the dialog? We’ll get an exception, *but even though we’ve had an exception, we still want to close the turtle’s window*. Lines 17–18 does this for us. Whether we complete the statements in the `try` clause successfully or not, the `finally` block will always be executed. Notice that the exception is still unhandled — only an `except` clause can handle an exception, so our program will still crash. But at least its turtle window will be closed before it crashes! ## 13.9. Glossary exception : An error that occurs at runtime. handle an exception : To prevent an exception from terminating a program by wrapping the block of code in a `try` / `except` construct. raise : To cause an exception by using the `raise` statement. ## 13.10. Exercises Write a function named `readposint` that uses the `input` dialog to prompt the user for a positive integer and then checks the input to confirm that it meets the requirements. It should be able to handle inputs that cannot be converted to `int`, as well as negative `int`, and edge cases (e.g. when the user closes the dialog, or does not enter anything at all.)