Part VII deals with exceptions, which are events that can modify the flow of control through a program. In Python, exceptions are triggered automatically on errors, and can be both triggered and intercepted by your code. They are processed by three statements we’ll study in this part, the first of which has two variations:
try
/except
Catch and recover from exceptions raised by Python, or by you.
try
/finally
Perform cleanup actions whether exceptions occur or not.
raise
Trigger an exception manually in your code.
assert
Conditionally trigger an exception in your code.
With a few exceptions (pun intended), we’ll find that exception handling is simple in Python, because it’s integrated into the language itself as another high-level tool.
In a nutshell, exceptions let us jump out of arbitrarily large chunks of a program. Consider the pizza-making robot we talked about earlier in the book. Suppose we took the idea seriously and actually built such a machine. To make a pizza, our culinary automaton would need to execute a plan, which we implement as a Python program. It would take an order, prepare the dough, add toppings, bake the pie, and so on.
Now, suppose that something goes very wrong during the “bake the pie” step. Perhaps the oven is broken. Or perhaps our robot miscalculates its reach and spontaneously bursts into flames. Clearly, we want to be able to jump to code that handles such states quickly. Since we have no hope of finishing the pizza task in such unusual cases, we might as well abandon the entire plan.
That’s exactly what exceptions let you do; you can
jump to an exception handler in a single step, abandoning all
suspended function calls. They’re a sort of
structured “super-goto.”[1] An exception handler (try
statement)
leaves a marker and executes some code. Somewhere further ahead in
the program, an exception is raised that makes Python jump back to
the marker immediately, without resuming any active functions that
were called since the marker was left. Code in the exception handler
can respond to the raised exception as appropriate (calling the fire
department, for instance). Moreover, because Python jumps to the
handler statement immediately, there is usually no need to check
status codes after every call to a function that could possibly fail.
In Python programs, exceptions are typically used for a variety of purposes. Here are some of their most common roles:
Python raises exceptions whenever it detects errors in programs at runtime. You can
either catch and respond to the errors in your code, or ignore the
exception. If the error is ignored, Python’s default
exception-handling behavior kicks in—it stops the program and
prints an error message. If you don’t want this
default behavior, code a try
statement to catch
and recover from the exception—Python jumps to your
try
handler when the error is detected, and your
program resumes execution after the try
.
Exceptions can also signal a valid condition, without having to pass result flags around a program or test them explicitly. For instance, a search routine might raise an exception on failure, rather than returning an integer result code (and hoping that the code will never be a valid result).
Sometimes a condition may happen so rarely that it’s hard to justify convoluting your code to handle it. You can often eliminate special-case code by handling unusual cases in exception handlers instead.
As we’ll see, the
try
/finally
statement allows us
to guarantee that required closing-time operations will be performed,
regardless of the presence or absence of exceptions in our program.
And finally, because exceptions are a sort of high-level “goto,” you can use them as the basis for implementing exotic control flows. For instance, although backtracking is not part of the language itself, it can be implemented in Python with exceptions and a bit of support logic to unwind assignments.[2]
We’ll see such typical uses in action later in this part of the book. First, let’s get started with a look at Python’s exception-processing tools.
Compared to some other core language topics we’ve met, exceptions are a fairly light-weight tool in Python. Because they are so simple, let’s jump right into an initial example. Suppose you’ve coded the following function:
>>>def fetcher(obj, index):
...return obj[index]
There’s not much to this function—it simply indexes an object on a passed-in index. In normal operation, it returns the result of legal indexes:
>>>x = 'spam'
>>>fetcher(x, 3) # Like x[3]
'm'
However, if you ask this function to index off the end of your
string, you will trigger an exception when your function tries to run
obj[index]
. Python detects out-of-bounds sequence
indexing, and reports it by raising (triggering)
the built-in
IndexError
exception:
>>> fetcher(x, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in fetcher
IndexError: string index out of range
Technically, because this exception is not caught by your code, it reaches the top level of the program and invokes the default exception handler— which simply prints the standard error message. By this point in the book, you’ve probably seen your share of standard error messages. They include the exception that was raised, along with a stack trace—a list of the lines and functions active when the exception occurred. When coding interactively, the file is just “stdin” (standard input stream) or “pyshell” (in IDLE), so file line numbers are not very meaningful here.
In a more realistic program launched outside the interactive prompt,
the default handler at the top also terminates
the
program immediately. That course of action makes sense for simple
scripts; errors often should be fatal, and the best you can do is
inspect the standard error message. Sometimes this
isn’t what you want, though. Server programs, for
instance, typically need to remain active even after internal errors.
If you don’t want the default exception behavior,
wrap the call in a try
statement to catch the
exception yourself:
>>>try:
...fetcher(x, 4)
...except IndexError:
...print 'got exception'
... got exception >>>
Now, Python jumps to your handler (the block
under the except
clause that names the exception
raised) automatically when the exception is triggered while the
try
block runs. When working interactively like
this, after the except
clause runs we wind up back
at the Python prompt. In a more realistic program,
try
statements not only catch
exceptions, but also recover from them:
>>>def catcher( ):
...try:
...fetcher(x, 4)
...except IndexError:
...print 'got exception'
...print 'continuing'
... >>>catcher( )
got exception continuing >>>
This time, after the exception is caught and handled, the program
resumes execution after the entire try
statement
that caught it—which is why we get the
“continuing” message here. You
won’t see the standard error message, and your
program will continue on its way normally.
Exceptions can be raised both by Python and you, and can be caught or
not. To trigger an exception manually, simply run a
raise
(or assert
) statement;
user-defined exceptions are caught the same way as built-ins:
>>>bad = 'bad'
>>>try:
...raise bad
...except bad:
...print 'got bad'
... got bad
If not caught, user-defined exceptions reach the top-level default exception handler, and terminate your program with a standard error message. In this case, the standard message includes the text of the string used to identify the exception:
>>> raise bad
Traceback (most recent call last):
File "<pyshell#18>", line 1, in ?
raise bad
bad
In other cases, the error message may include text provided by classes used to identify exceptions. As we’ll see in the next chapter, class-based exceptions allow scripts to build exception categories (and other things):
>>>class Bad: pass
... >>>def doomed( ): raise Bad( )
... >>>try:
...doomed( )
...except Bad:
...print 'got Bad'
... got Bad >>>
Finally, try
statements can also say
finally
: the
try
/finally
combination
specifies termination actions that always execute
“on the way out”—whether an
exception happens in the try
block or not:
>>>try:
...fetcher(x, 3)
...finally:
...print 'after fetch'
... 'm' after fetch
Here, when the try
block finishes without an
exception, the finally
block runs, and then the
program resumes after the entire try
. In this
case, this statement seems a bit silly—we might as well have
simply typed the print
right after a call to the
function, and skipped try
altogether:
fetcher(x, 3) print 'after fetch'
There is a problem with coding this way, though: if the function call
raises an exception, we will never reach the
print
. The
try
/finally
lets us do
better—when an exception does occur in the
try
block, finally
blocks are
executed while the program is being unwound:
>>>def after( ):
...try:
...fetcher(x, 4)
...finally:
...print 'after fetch'
...print 'after try?'
... >>>after( )
after fetch Traceback (most recent call last): File "<stdin>", line 1, in ? File "<stdin>", line 3, in after File "<stdin>", line 2, in fetcher IndexError: string index out of range
Here, we didn’t get the “after
try?” message, because control does not resume after
the try
/finally
when an
exception occurs. Instead, Python jumps back to run the
finally
action, but then keeps
propagating
the exception to a prior handler (in
this case, to the default handler at the top). If you change the call
inside this function so as not to trigger an exception, the
finally
code still runs, but the program continues
after the try
:
>>>def after( ):
...try:
...fetcher(x, 3)
...finally:
...print 'after fetch'
...print 'after try?'
... >>>after( )
after fetch after try? >>>
In practice, try
/except
combinations are useful for catching and recovering from exceptions,
and try
/finally
comes in handy
to guarantee that termination actions will fire regardless of the
exceptions that may occur in the try
block’s code. For instance, you might use
try
/except
to catch errors
raised by code that you import from a third-party library, and
try
/finally
to ensure that
calls to close files or terminate server connections are always run.
We’ll see some such practical examples later in this
part of the book.
That is a majority of the exception story; it really is a simple
tool. In the rest of this part, we’ll fill in some
of the details of the statements involved, show you the other sorts
of clauses that can appear under a try
, and
discuss string and class-based exception objects.
Python exceptions are a high-level control flow device. They may be
raised either by Python or by your own programs; in both cases, they
may be ignored (to trigger the default error message), or may be
caught by try
statements (to be processed by your
code). Python’s raise
and
assert
statements trigger exceptions on demand.
The try
statement comes in two formats—one
that handles exceptions and one that executes finalization code
whether exceptions occur or
not. Let’s take a
deeper look at these statements’ general forms.
The try
is another compound
statement;
its most complete form is sketched below. It starts with a
try
header line followed by a block of (usually)
indented statements, then one or more except
clauses that identify exceptions to be caught, and an optional
else
clause at the end. The words
try
, except
, and
else
are associated by indenting them the
same—they line up vertically. For reference,
here’s the general format:
try: <statements> # Run this action first. except <name1>: <statements> # Run if name1 is raised during try block. except <name2>, <data>: <statements> # Run if name2 is raised, and get extra data. except (name3, name4): <statements> # Run if any of these exceptions occur. except: <statements> # Run for all (other) exceptions raised. else: <statements> # Run if no exception was raised by try block.
In this statement, the block under the try
header
represents that main action of the
statement—the code you’re trying to run. The
except
clauses define
handlers for exceptions raised during the
try
block, and the else
clause
(if coded) provides a handler to be run if no
exception occurs. The <data>
entry here has
to do with a feature of raise
statements we will
discuss later in this chapter.
Here’s how try
statements work.
When a try
statement is started, Python marks the
current program context, so it can come back if an exception occurs.
The statements nested under the try
header are run
first. What happens next depends on whether exceptions are raised
while the try
block’s statements
are running:
If an exception occurs while the try
block’s statements are running, Python jumps back to
the try
and runs the statements under the first
except
clause that matches the raised exception.
Control continues past the entire try
statement
after the except
block runs (unless the
except
block raises another exception).
If an exception happens in the try
block and
no
except
clause matches, the
exception is propagated up to a try
that was
entered earlier in the program, or to the top level of the process
(which makes Python kill the program and print a default error
message).
If no exception occurs while the statements under the
try
header run, Python runs the statements under
the else
line (if present), and control then
resumes past the entire try
statement.
In other words, except
clauses catch exceptions
that may happen while the try
block is running,
and the else
clause is run only if no exceptions
happen while the try
block runs.
The except
clauses are focused exception
handlers—they catch exceptions that occur only within the
statements in the associated try
block. However,
since the try
block’s statements
can call functions coded elsewhere in a program, the source of an
exception may be outside the try
statement itself.
More on this when we explore try
nesting in Chapter 26.
When you write
try
statements, a
variety of clauses can appear after the try
statement block; Table 24-1 summarizes all the
possible forms, and you must use at least one. We’ve
already met some of these—except
clauses
catch exceptions, finally
runs on the way out, and
so on. Syntactically, there may be any number of
except
clauses, but there should be only one
else
. Moreover, the finally
clause must appear alone (without else
or
except
); it’s really a different
statement.
Clause form |
Interpretation |
|
Catch all (other) exception types. |
|
Catch a specific exception only. |
|
Catch exception and its extra data. |
|
Catch any of the listed exceptions. |
|
Catch any, and get the extra data. |
|
Run block if no exceptions raised. |
|
Always perform block. |
We’ll explore the entries with the extra
value
part when we meet the
raise
statement. The first and fourth entries in
Table 24-1 are new here:
except
clauses that list no exception name catch
all exceptions not previously listed in the
try
statement (except
:).
except
clauses that list a set of exceptions in
parenthesis catch any of the listed exceptions
(except (e1,e2,e3)
).
Since Python looks for a match within a given try
by inspecting except
clauses from top to bottom,
the parenthesized version is like listing each exception in its own
except
clause, but the statement body needs to be
coded only once. Here’s an example of multiple
except clauses at work, which demonstrates just how specific your
handlers can be:
try: action( ) except NameError: ... except IndexError ... except KeyError: ... except (AttributeError, TypeError, SyntaxError): ... else: ...
In this example, when an exception is raised while the call to the
action
function is running, Python returns to the
try
and searches for the first
except
that names the exception raised. It
inspects except
clauses from top to bottom and
left to right, and runs the statements under the first one that
matches. If none match, the exception is propagated past this
try
.
Note that the else
runs only when
no exception occurred in
action
, not for other exceptions raised. If you
really want a general catch-all clause, an
empty
except
does the trick:
try: action( ) except NameError: ... # Handle NameError. except IndexError ... # Handle IndexError. except: ... # Handle all other exceptions. else: ... # Handle the no-exception case.
The empty except
clause is a sort of wildcard
feature—because it catches everything, it allows your handlers
to be as general or specific as you like. In some scenarios, this
form may be more convenient than listing all possible exceptions in a
try
. For example, the following catches everything
without listing anything:
try: action( ) except: ... # Catch all possible exceptions.
Empty excepts
also raise some design issues.
Although convenient, they may also catch unexpected system exceptions
unrelated to your code, and may inadvertently intercept exceptions
meant for another handler. For example, even system exit calls in
Python trigger exceptions, and you usually want these to pass.
We’ll revisit this as a gotcha at the end of Part VII. For now, we’ll just say:
use with care.
At first glance, the
purpose
of the else
clause is not always obvious. Without
it, though, there is no way to tell, without setting and checking
Boolean flags, whether we wound up past a try
statement because no exception happened, or because an exception
occurred and was handled:
try: ...run code... except IndexError: ...handle exception... # Did we get here because the try failed or not?
Much like else
clauses in loops, the
else
provides syntax here that makes this case
obvious and unambiguous:
try: ...run code... except IndexError: ...handle exception... else: ...no exception occurred...
You can almost emulate an
else
clause by moving its code to the end of the
try
block:
try: ...run code... ...no exception occurred... except IndexError: ...handle exception...
This can lead to incorrect exception classifications, though. If the
“no exception occurred” action
triggers IndexError
, it will register as a failure
of the try
block, and hence erroneously trigger
the exception handler below the try
(subtle, but
true!). By using an explicit else
clause instead,
you make the logic more obvious, and guarantee that
except
handlers only run for real failures in the
code you’re wrapping in try
, not
for failures in the else
case’s
action.
Since control flow through a program is easier to capture in Python
than in English, let’s run some examples that
further illustrate exception
basics. Exceptions
not caught by try
statements reach the top level
of a Python process and run Python’s default
exception-handling logic. Python terminates the running program and
prints a standard error message. For example, running the following
module, bad.py, generates a divide-by-zero
exception:
def gobad(x, y): return x / y def gosouth(x): print gobad(x, 0) gosouth(1)
Since the program ignores the exception it triggers, Python kills the program and prints a message—this time, with useful file and line number information:[3]
% python bad.py
Traceback (most recent call last):
File "bad.py", line 7, in ?
gosouth(1)
File "bad.py", line 5, in gosouth
print gobad(x, 0)
File "bad.py", line 2, in gobad
return x / y
ZeroDivisionError: integer division or modulo by zero
When an uncaught exception occurs, Python ends the program, and
prints a stack trace and the name and any extra data of the exception
that was raised. The stack trace lists all lines active when the
exception occurred, from oldest to newest. For example, you can see
that the bad divide happens at the last entry in the trace—line
2 of file bad.py, a return
statement.
Because Python detects and reports all errors at runtime by raising
exceptions, exceptions are intimately bound up with the idea of error
handling in general. For instance, if you’ve worked
through the examples, you’ve undoubtedly seen an
exception or two along the way—even typos usually generate a
SyntaxError
or other exception when a file is
imported or executed (that’s when the compiler is
run). By default, you get a useful error display like the one above,
which helps track down the problem.
Often this standard error message is all you need to resolve a
problem in your code. For more heavy duty debugging jobs, you can
catch exceptions with try
statements, or use
debugging tools we’ll introduce in Chapter 26.
Python’s default exception handling is often exactly what you want—especially for code in top-level script files, an error generally should terminate your program immediately. For many programs, there is no need to be more specific about errors in your code.
Sometimes, though, you’ll want to catch errors and
recover from them instead. If you don’t want your
program terminated when an exception is raised by Python, simply
catch it by wrapping program logic in a try
. For
example, the following code catches and recovers from the
TypeError
Python raises immediately when we try to
concatenate a list and a string (the +
operator
wants the same sequence type on both sides):
def kaboom(x, y): print x + y # Trigger TypeError. try: kaboom([0,1,2], "spam") except TypeError: # Catch and recover here. print 'Hello world!' print 'resuming here' # Continue here if exception or not.
When the exception occurs in function kaboom
,
control jumps to the try
statement’s except
clause, which
prints a message. Since an exception is
“dead” after it’s
been caught like this, the program continues past the whole
try
, rather than being terminated by Python. In
effect, your code processes and clears the error.
Notice that once you’ve caught the error, control
resumes at the place where you caught it, after the
try
; there is no direct way to go back to the
place where the exception occurred (function
kaboom
). In a sense, this makes exceptions more
like simple jumps than function calls—there is no way to return
to the code that triggered the error.
The other flavor of the try
statement is
a
specialization and has to do with finalization actions. If a
finally
clause is used in a
try
, its block of statements are always run by
Python “on the way out,” whether an
exception occurred while the try
block was running
or not. Its general form:
try: <statements> # Run this action first. finally: <statements> # Always run this code on the way out.
Here’s how this variant works. Python begins by
running the statement block associated with the
try
header line first. The remaining behavior of
this statement depends on whether an exception occurs during the
try
block or not:
If no exception occurs while the try
block is
running, Python jumps back to run the finally
block, and then continues execution past the entire
try
statement.
If an exception does occur during the
try
block’s run, Python comes
back and runs the finally
block, but then
propagates the exception to a higher try
or the
top-level default handler; the program does not resume execution past
the try
statement.
The try
/finally
form is useful
when you want to be completely sure that an action happens after some
code runs, regardless of the exception behavior of the program. Note
that the finally
clause cannot be used in the same
try
statement as except
and
else
, so it is best thought of as a distinct
statement form.
We saw simple try
/finally
examples
earlier. Here’s a
more realistic example that illustrates a typical role for this
statement:
MyError = "my error" def stuff(file): raise MyError file = open('data', 'r') # Open an existing file. try: stuff(file) # Raises exception finally: file.close( ) # Always close file. ... # Continue here if no exception.
In this code, we’ve wrapped a call to a
file-processing function in a try
with a
finally
clause, to make sure that the file is
always closed, whether the function triggers an exception or not.
This particular example’s function
isn’t all that useful (it just raises an exception),
but wrapping calls in
try
/finally
statements is a
good way to ensure that your closing-time (i.e., termination)
activities always run. Python always runs the code in your
finally
blocks, regardless of whether an exception
happens in the try
block or not.[4] For example, if the function here did not
raise an exception, the program would still execute the
finally
block to close your file,
and then
continue past the entire try
statement.
To trigger
exceptions
explicitly, you code raise
statements. Their
general form is simple—the word raise
,
optionally followed by both the name of the exception to be raised
and an extra data item to pass with the exception:
raise <name> # Manually trigger an exception. raise <name>, <data> # Pass extra data to catcher too. raise # Reraise the most recent exception.
The second form allows you to pass an extra data item along with the
exception, to provide details for the handler. In the
raise
, the data is listed after the exception
name; back in the try
statement, the data is
obtained by including a variable to receive it. For instance, in
except name,X
:, X
will be
assigned the extra data item listed at the raise
.
The third raise form simply reraises the current exception;
it’s handy if you want to propagate an exception
you’ve caught to another handler.
So what’s an exception name? It might be the name of
a built-in exception from the built-in scope (e.g.,
IndexError
), or the name of an arbitrary string
object you’ve assigned in your program. It can also
reference a user-defined class or class instance—a possibility
that further generalizes raise
statement formats.
We’ll postpone the details of this generalization
until after we have a chance to study class exceptions in the next
chapter.
Regardless of how you name exceptions, they are always identified by
normal objects, and at most one is active at any given time. Once
caught by an except
clause anywhere in the
program, an exception dies (won’t propagate to
another try
), unless reraised by another
raise
statement or error.
Python programs can trigger both
built-in and user-defined
exceptions, using the raise
statement. In their
simplest form, user-defined exceptions are string objects, like the
one that variable MyBad
is assigned to in the
following:
MyBad = "oops" def stuff( ): raise MyBad # Trigger exception manually. try: stuff( ) # Raises exception except MyBad: print 'got it' # Handle exception here. ... # Resume execution here.
This time, the raise
occurs inside a function, but
it makes no real difference—control jumps back to the
except
block immediately. Notice that user-defined
exceptions are caught with try
statements just
like built-in exceptions.
As suggested earlier,
raise
statements can pass an extra data
item along with the exception for use in a handler. In general,
the extra data allows you to send context
information about the exception to a handler. If
you’re writing a data file parser, for example, you
might raise a syntax error exception on errors, and also pass along
an object that gives line and file information to the handler
(we’ll meet an example of such later in this part).
Strictly speaking, every exception has the extra data: much like
function return values, it defaults to the special
None
object if nothing was passed explicitly. The
following code, raisedata.py, illustrates this
concept at work:
myException = 'Error' # String object def raiser1( ): raise myException, "hello" # Raise, pass data. def raiser2( ): raise myException # Raise, None implied. def tryer(func): try: func( ) except myException, extraInfo: # Run func; catch exception+data. print 'got this:', extraInfo %python
>>>from raisedata import *
>>>tryer(raiser1) # Explicitly passed extra data
got this: hello >>>tryer(raiser2) # Extra data is None by default.
got this: None
Here, the tryer
function always requests the extra
data object; it comes back as an explicit string from
raiser1
, but defaults to None
in raiser2
’s
raise
statement. Later, we’ll see
that the same hook can be used to access instances raised in
conjunction with class-based exceptions.
A raise
statement without an
exception name or extra data value simply
reraises the current exception. It’s typically used
if you need to catch and handle an exception, but
don’t want the exception to die in your code:
>>>try:
...raise IndexError, 'spam'
...except IndexError:
...print 'propagating'
...raise
... propagating Traceback (most recent call last): File "<stdin>", line 2, in ? IndexError: spam
By running a raise
this way, the exception will be
reraised and thus propagated to a higher handler, or to the
default
handler at the top, which stops the program with a standard error
message.
As a somewhat special case, Python includes the
assert
statement. It is mostly syntactic shorthand for a common
raise
usage pattern, and can be thought of as a
conditional raise statement. A statement of the
form:
assert <test>, <data> # The <data> part is optional.
works like the following code:
if __debug__: if not <test>: raise AssertionError, <data>
In other words, if the test evaluates to false, Python raises an
exception, with the data item as the exception’s
extra data (if provided). Like all exceptions, the assertion error
exception raised will kill your program if not caught with a
try
.
As an added feature, assert
statements may also be
removed from the compiled program’s byte code if the
-O
Python command-line flag is used, thereby
optimizing the program. AssertionError
is a
built-in exception, and the __debug__
flag is a
built-in name that is automatically set to 1
(true) unless the -O
flag is used.
Assertions are typically used to verify program
conditions during development. When
displayed, their error message text automatically includes source
code line information, and the value you listed in the
assert
statement. Consider
asserter.py:
def f(x): assert x < 0, 'x must be negative' return x ** 2 %python
>>>import asserter
>>>asserter.f(1)
Traceback (most recent call last): File "<stdin>", line 1, in ? File "asserter.py", line 2, in f assert x < 0, 'x must be negative' AssertionError: x must be negative
It’s important to keep in mind that
assert
is mostly intended for trapping
user-defined constraints, not for catching genuine programming
errors. Because Python traps programming errors itself, there is
usually no need to code asserts
to catch things
like out-of-bounds indexes, type mismatches, and zero divides:
def reciprocal(x): assert x != 0 # a useless assert! return 1 / x # python checks for zero automatically
Such asserts
are generally superfluous. Because
Python raises exceptions on errors automatically, you might as well
let Python do the job for you.[5] For
another example of assert
common usage, see the
abstract superclass example of Chapter 21; there,
we used assert
to make calls to undefined methods
fail with a message.
[1] If you’ve used C, you may be interested to
know that Python exceptions are roughly similar to
C’s setjmp/longjmp
standard
function pair. The try
statement acts much like a
setjmp
, and raise
works like a
longjmp
. But in Python, exceptions are based on
objects and are a standard part of the execution model.
[2] True backtracking is an advanced
topic that is not part of the Python language (even with addition of
generator functions in 2.2), so we won’t say more
about it here. Roughly, backtracking undoes all computation before it
jumps; Python exceptions do not (e.g., variables assigned between the
time a try
statement is entered and an exception
is raised are not reset to their prior values). See a book on
artificial intelligence or the Prolog or icon programming languages
if you’re curious.
[3] We should point out that the text of error messages and stack traces tends to vary slightly over time. Don’t be alarmed if your error messages don’t exactly match ours.
[4] Unless Python crashes completely, of course. Python does a good job of avoiding crashes, by checking all possible errors as a program runs. When a program does crash hard, it is often due to a bug in linked-in C extension code, outside of Python’s scope.
[5] In most cases, at least. As suggested in Part IV, if a function has to perform long-running or unrecoverable actions before it reaches the place where an exception will be triggered, you still might want to test for errors. Even in this case, though, be careful not to make your tests overly specific or restrictive, or you will limit your code’s utility.