The remainder of this chapter will be devoted to Python’s built-in exception mechanism. The entire Python exception mechanism is built around object-oriented specifications, which makes it both flexible and extensible. Even if you are not familiar with object-oriented programming (OOP), you don’t have to go out of your way to learn object-oriented techniques when using exceptions.
Exceptions are objects automatically generated by Python functions using the raise statement. After the exception object is generated, the raise statement that raises the exception changes the way the Python program executes, unlike normal execution. Instead of continuing to execute the next statement of Raise or the one after the exception is generated, you retrieve the current function call chain to find a handler that can handle the current exception. If an exception handler is found, it is called and the exception object is accessed for more information. If no suitable exception handler is found, the program aborts with an error.
{Easier to ask than to admit! }
In general, Python treats error handling differently than is common in languages like Java. Those languages rely on checking for errors as much as possible before they occur, because handling exceptions after an error has occurred is often costly. This approach, described in the first section of this chapter, is sometimes referred to as “Look Before You Leap” (LBYL).
Python, on the other hand, may rely more on “exceptions,” which are handled after an error has occurred. While this dependence may seem risky, if “exceptions” are used properly, the code is lighter and more readable, and only handled when an error occurs. This Python-style approach to handling errors is often called Easier to Ask Forgiveness than Permission (EAFP).
14.2.1 Types of Python Exceptions
Different types of exception objects can be generated to accurately reflect the actual cause of the error or the exception that needs to be reported. Python 3.6 provides many types of exception objects:
Copy the code
BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
StopIteration
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AssertionError
AttributeError
BufferError
EOFError
ImportError
ModuleNotFoundError
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
BytesWarningException
ResourceWarning
Python’s exception objects are built hierarchically, as illustrated by the indentation in the exception list above. As shown in Chapter 10, you can get an alphabetical list of exception objects from the __builtins__ module.
Each exception is a Python class that inherits from the parent exception class. But if you haven’t been exposed to OOP yet, don’t worry. For example, IndexError is also a LookupError class and an Exception class (by inheritance), and a BaseException.
This hierarchy is intentional; most exceptions inherit from Exception, and it is strongly recommended that all user-defined exceptions should also be subclasses of Exception, not BaseException. The reasons are as follows.
Copy the code
try:
# Perform some exception operations
except Exception:
# handle exception
{: -} In the above code, you can still abort the try block with Ctrl+C without raising exception handling code. Because KeyboardInterrupt is not a subclass of Exception.
While explanations for each exception can be found in the documentation, the most common exceptions are quickly familiar through hands-on programming.
14.2.2 Raising an Exception
Exceptions can be thrown by many of Python’s built-in functions:
Copy the code
>>> alist = [1, 2, 3]
>>> element = alist[7]
Traceback (innermost last):
File "<stdin>", line 1, in ?
IndexError: list index out of range
Python’s built-in error-checking code will detect that the list index value requested for the second line does not exist and raise IndexError. The exception goes all the way back to the top level, the interactive Python interpreter, which processes it by printing out a message indicating that an exception has occurred.
You can also use the RAISE statement to explicitly raise an exception in your own code. The basic form of the raise statement is as follows:
Copy the code
raise exception(args)
The Exception (args) section creates an exception object. The parameters of the new exception object should generally be values that help determine the error condition, as described later. After the exception object is created, raise throws it up the Python function stack, which is the function currently executing into the Raise statement. The newly created exception is thrown to the most recent type-matching exception catching block on the stack. If no corresponding exception catching block is found up to the top level of the program, the program stops running with an error message, which is printed to the console in an interactive session.
Try the following code:
Copy the code
>>> raise IndexError("Just kidding")
Traceback (innermost last):
File "<stdin>", line 1, in ?
IndexError: Just kidding
At first glance, the message generated with Raise above looks similar to all of the Python list index error messages that have come before. A closer look reveals that this is not the case. The actual mistake was not as serious as the previous one.
String arguments are often used when creating exceptions. If the first argument is given, most built-in Python exceptions assume that it is information to display as an explanation of what has happened. This is not always the case, however, because each exception type has its own class, and the parameters required to create an exception for that class are entirely determined by the class definition. In addition, custom exceptions created by programmers are often used for purposes other than error handling and may not take text information as arguments.
14.2.3 Catching and handling exceptions
The point of the exception mechanism is not to abort a program with an error message. It has never been difficult to implement abort functionality in programs. The exception mechanism is special in that it does not necessarily stop the program from running. By defining appropriate exception-handling code, you can ensure that common exceptions do not cause your program to fail. You might be able to show the user an error message or something, and you might be able to fix the problem, but not crash the program.
The following shows the basic syntax for Python exception catching and handling, using the try, except, and sometimes else keyword:
Copy the code
try:
body
except exception_type1 as var1:
exception_code1
except exception_type2 as var2:
exception_code2
.
.
.
except:
default_exception_code
else:
else_body
finally:
finally_body
The body part of the try statement is executed first. If the execution succeeds, that is, the try statement does not catch any exception thrown, the else_body part is executed and the try statement completes. Because there is a finally statement, the finally_body part is then executed. If an exception is thrown at a try, each except clause is searched in turn for a clause that matches the type of exception associated with the exception thrown. If a matching except clause is found, the thrown exception is assigned to the variable, whose name is given after the associated exception type, and the exception-handling code that matches the except clause is executed. For example, except Exception_type as var: This line matches a thrown exception exc, creating the variable var and assigning the value of var to exc before executing the exception-handling code of the except statement. Var is not required and only except Exception_type can occur: written this way, an exception of a given type can still be caught, but the exception is not assigned to a variable.
If no matching except clause is found, the try statement cannot handle the exception thrown, and the exception continues to be thrown further up the function call chain, expecting an outer try to handle it.
The last except clause in the try statement can specify no exception type at all, thus handling all types of exceptions. This technique can be handy for some debugging work and very rapid prototyping. But this is usually not a good idea, as all errors are covered up by an except clause, which can make some of the program’s behavior difficult to understand.
The else clause of the try statement is optional and rarely used. The else clause is executed if and only if the body part of the try statement is executed without throwing any errors.
The finally clause of a try statement is also optional, executed after the try, except, and else parts have all been executed. If an exception is raised ina try block and has not been handled by any except block, the finally block will raise the exception again after it completes execution. Because the finally block is always executed, it provides an opportunity to add resource cleaning code after exception handling is complete by closing files, resetting variables, and the like.
Write code to read two numbers entered by the user and divide the first number by the second number. Check and catch exceptions when the second digit is 0 (ZeroDivisionError).
14.2.4 Customizing New Exceptions
Defining your own exceptions is simple. This can be done with two lines of code:
Copy the code
class MyError(Exception):
pass
{: -} This code creates a class that inherits everything from the base class Exception. But if you don’t want to know the details, don’t bother.
The above exceptions can be thrown, caught, and handled just like any other exception. If a parameter is given and not captured and processed, the value of the parameter is printed at the end of the trace:
Copy the code
>>> raise MyError("Some information about what went wrong")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
__main__.MyError: Some information about what went wrong
Of course, the above parameters are also accessible in your own exception handling code:
Copy the code
try:
raise MyError("Some information about what went wrong")
except MyError as error:
print("Situation:", error)
The result should look like this:
Copy the code
Situation: Some information about what went wrong
If an exception is thrown with more than one parameter, these parameters are passed into the exception handling code as tuples, accessible via the error variable’s args attribute:
Copy the code
try:
raise MyError("Some information", "my_filename", 3)
except MyError as error:
print("Situation: {0} with file {1}\n error code: {2}".format(
error.args[0],
error.args[1], error.args[2]))
The result should look like this:
Copy the code
Situation: Some information with file my_filename
error code: 3
Exception types are regular Python classes and inherit from Exception classes, so it’s a simple matter to create your own Exception type hierarchy for your own code to use. When reading this book for the first time, don’t worry about this process. Come back anytime after you finish chapter 15. How you create your own exceptions is entirely up to your requirements. If you’re writing a small program that might generate only unique errors or exceptions, use a subclass of the Exception class as described above. If you’re writing a large, multi-file code base that performs a specific function (such as a weather forecast library), consider defining a separate class called WeatherLibraryException and subclassing all the different exceptions in the library.
Except Exception as e except MyError as e except MyError as e
14.2.5 Debug programs with Assert statements
The assert statement is a special form of the raise statement:
Copy the code
assert expression, argument
If expression is settled to False and the system variable __debug__ is also True, an AssertionError with the optional argument is raised. The __debug__ variable defaults to True. Starting the Python interpreter with -o or -oo arguments, or setting the system variable PYTHONOPTIMIZE to True, sets __debug__ to False. The optional argument can be used to place explanation information for the assert.
If __debug__ is False, the code generator will not create code for assert statements. During development, you can use assert statements in conjunction with debug statements to test your code. Assert statements can be retained in code for future use and have no runtime overhead in normal use:
Copy the code
>>> x = (1, 2, 3)
>>> assert len(x) > 5, "len(x) not > 5"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: len(x) not > 5
Write a simple program that asks the user to enter a number and use the assert statement to raise an exception when the number is 0. First test to ensure that assert statements execute, and then disable Assert using the methods described in this section.
14.2.6 Exception Inheritance Architecture
As described earlier, Exceptions in Python are layered architectures. This section provides an in-depth look at this architecture, including its implications for how except clauses catch exceptions.
Take a look at the following code:
Copy the code
try:
body
except LookupError as error:
exception code
except IndexError as error:
exception code
Both IndexError and LookupError exceptions will be caught. As it happens, IndexError is a subclass of LookupError. If the body throws IndexError, the error is first detected by the “except LookupError as error:” line. Since IndexError inherits from LookupError, the first except clause executes successfully, and the second except clause is never used because its running conditions are included in the first except clause.
Instead, it might make sense to reverse the order of the two except clauses. Thus the first clause handles IndexError, and the second clause handles LookupError except IndexError.
14.2.7 Example: Disk writer in Python
This section returns to the example of a word processor that needs to check for insufficient disk space when writing a document to disk:
Copy the code
def save_to_file(filename) :
try:
save_text_to_file(filename)
save_formats_to_file(filename)
save_prefs_to_file(filename)
.
.
.
except IOError:
. Handling errors...
def save_text_to_file(filename):
. Call the underlying function to write the text size...
. Call the underlying function to write the actual text data...
.
.
.
Note that the error-handling code is unobtrussed in the save_to_file function along with a series of disk write calls. None of those disk-writing subfunctions need to include any error-handling code. The program will be easy to develop initially, and it will be easy to add error-handling code later. Programmers do this all the time, even if the order is not optimal.
It is also worth noting that the code above does not respond only to disk full errors, but to all IOError exceptions. Python’s built-in functions automatically raise IOError whenever they fail to complete an I/O request, for whatever reason. This may suffice, but if you want to identify disk full separately, you have to do something more. You can check how much free space your disk has in the except body. If there is insufficient disk space, it is clear that a disk full problem has occurred and should be handled in the except statement body. If disk space is not a problem, the code in the EXCEPT body can throw this IOError up the calling chain for the other EXCEPT body to handle. If this solution is not enough, you can do something more extreme, such as finding the C source code for the Python disk-writing function and throwing custom DiskFull exceptions as needed. This last option is not recommended, but it makes sense to be aware of the possibility if necessary.
14.2.8 Example: An Exception occurs during normal computing
The most common use of exceptions is to handle errors, but they can also be useful in situations that should be considered normal computation. Imagine the problems you might encounter when implementing something like a spreadsheet program. Like most spreadsheets, the program must be able to perform arithmetic operations involving multiple cells and allow non-numeric values in the cells. In such an application, the contents of a blank cell encountered during numerical computation may be treated as a value of 0. Cells that contain any other non-numeric string may be considered invalid and represented as Python’s None value. Any calculation involving an invalid value should return an invalid value.
Let’s start by writing a function that evaluates a string in a spreadsheet cell and returns the appropriate value:
Copy the code
def cell_value(string):
try:
return float(string)
except ValueError:
if string == "":
return 0
else:
return None
Python’s exception-handling capabilities make this function simple to write. In the try block, the string in the cell is converted to a number using the built-in float function and the result is returned. If the argument string cannot be converted to a number, the float function raises ValueError. The exception-handling code then catches the exception and returns 0 or None, depending on whether the argument string is empty.
Sometimes you might have to deal with None when evaluating, which is the next step. In a programming language without exceptions, the conventional solution is to define a set of custom arithmetic evaluation functions, check for None, and replace the built-in functions with these custom functions to perform all spreadsheet calculations. However, this process can be time-consuming and error-prone. And it’s actually building an interpreter into the spreadsheet program, so it slows things down. A different approach is adopted in this project. All spreadsheet formulas can actually be Python functions that take the x and y coordinates of the evaluated cell and the spreadsheet itself, compute the result using standard Python arithmetic operators, and extract the necessary values from the spreadsheet with cell_value. We can define a function called safe_apply that calls a formula with arguments in the try block and returns either the formula’s result or None, depending on whether it evaluated successfully:
Copy the code
def safe_apply(function, x, y, spreadsheet):
try:
return function(x, y, spreadsheet)
except TypeError:
return None
These two changes are sufficient to add the concept of None to the semantics of the spreadsheet. It would have been an instructive exercise (by implication, a considerable amount of work) to develop the above functionality without exception mechanics.
14.2.9 Application Scenarios of Exceptions
Using exceptions to handle almost any error is a natural solution. Error handling is often added when the rest of the program is almost complete. Unfortunately, this is the case, but exception mechanics are particularly good at writing such after-the-fact error-handling code in a way that’s easy to understand, or better yet, adding more after-the-fact error-handling code.
Exceptions can also be useful if it becomes apparent that a computational branch of the program is unsustainable, and then a large amount of processing may have to be discarded. This was the case with the spreadsheet example, and other scenarios included branch-and-bound and parsing.
Do Python exceptions force a program to abort?
Given that dictionary object X is being accessed, None is returned if the key does not exist, that is, KeyError is raised. How do you write code to do this?
Hands-on: Exception write code to create a custom ValueTooLarge and raise it when the variable x is greater than 1000.
This article is excerpted from Python Quickstart (version 3)
This is a Quick start to Python, written in Python 3.6. The book is divided into four parts. The first part covers the basics of Python and gives a brief introduction to Python. The second part introduces the focus of Python programming, including lists, tuples, collections, strings, dictionaries, flow control, functions, modules and scopes, file systems, exceptions, and so on. The third part explains the advanced features of Python, including classes and object-oriented, regular expressions, data types that are objects, packages, Python libraries, etc. The fourth part focuses on data processing, involving data file processing, network data, data preservation and data exploration, and finally gives relevant cases.
With a clear framework, well-organized content, step-by-step instructions, and a large number of examples and exercises, this book is a concise Python reference for both novice Python learners and professional programmers.