Chapter 24. Exception Basics

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.

Why Use Exceptions?

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.

Exception Roles

In Python programs, exceptions are typically used for a variety of purposes. Here are some of their most common roles:

Error handling

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.

Event notification

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).

Special-case handling

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.

Termination actions

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.

Unusual control-flows

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.

Exception Handling: The Short Story

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/except/else Statement

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.

Try Statement Clauses

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.

Table 24-1. try statement clause forms

Clause form

Interpretation

except:

Catch all (other) exception types.

except name:

Catch a specific exception only.

except name, value:

Catch exception and its extra data.

except (name1, name2):

Catch any of the listed exceptions.

except (name1, name2), value

Catch any, and get the extra data.

else:

Run block if no exceptions raised.

finally:

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.

The try/else Clause

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.

Example: Default Behavior

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.

Example: Catching Built-in Exceptions

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 try/finally Statement

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.

Example: Coding Termination Actions with try/finally

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.

The raise 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.

Example: Raising and Catching User-Defined Exceptions

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.

Example: Passing Extra Data with raise

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.

Example: Propagating Exceptions with raise

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.

The assert Statement

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.

Example: Trapping Constraints (but Not Errors)

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset