Exception Objects - Exceptions and Tools - Learning Python (2013)

Learning Python (2013)

Part VII. Exceptions and Tools

Chapter 35. Exception Objects

So far, I’ve been deliberately vague about what an exception actually is. As suggested in the prior chapter, as of Python 2.6 and 3.0 both built-in and user-defined exceptions are identified by class instance objects. This is what is raised and propagated along by exception processing, and the source of the class matched against exceptions named in try statements.

Although this means you must use object-oriented programming to define new exceptions in your programs—and introduces a knowledge dependency that deferred full exception coverage to this part of the book—basing exceptions on classes and OOP offers a number of benefits. Among them, class-based exceptions:

§ Can be organized into categories. Exceptions coded as classes support future changes by providing categories—adding new exceptions in the future won’t generally require changes in try statements.

§ Have state information and behavior. Exception classes provide a natural place for us to store context information and tools for use in the try handler—instances have access to both attached state information and callable methods.

§ Support inheritance. Class-based exceptions can participate in inheritance hierarchies to obtain and customize common behavior—inherited display methods, for example, can provide a common look and feel for error messages.

Because of these advantages, class-based exceptions support program evolution and larger systems well. As we’ll find, all built-in exceptions are identified by classes and are organized into an inheritance tree, for the reasons just listed. You can do the same with user-defined exceptions of your own.

In fact, in Python 3.X the built-in exceptions we’ll study here turn out to be integral to new exceptions you define. Because 3.X requires user-defined exceptions to inherit from built-in exception superclasses that provide useful defaults for printing and state retention, the task of coding user-defined exceptions also involves understanding the roles of these built-ins.

NOTE

Version skew note: Python 2.6, 3.0, and later require exceptions to be defined by classes. In addition, 3.X requires exception classes to be derived from the BaseException built-in exception superclass, either directly or indirectly. As we’ll see, most programs inherit from this class’s Exception subclass, to support catchall handlers for normal exception types—naming it in a handler will thus catch everything most programs should. Python 2.X allows standalone classic classes to serve as exceptions, too, but it requires new-style classes to be derived from built-in exception classes, the same as 3.X.

Exceptions: Back to the Future

Once upon a time (well, prior to Python 2.6 and 3.0), it was possible to define exceptions in two different ways. This complicated try statements, raise statements, and Python in general. Today, there is only one way to do it. This is a good thing: it removes from the language substantial cruft accumulated for the sake of backward compatibility. Because the old way helps explain why exceptions are as they are today, though, and because it’s not really possible to completely erase the history of something that has been used by on the order of a million people over the course of nearly two decades, let’s begin our exploration of the present with a brief look at the past.

String Exceptions Are Right Out!

Prior to Python 2.6 and 3.0, it was possible to define exceptions with both class instances and string objects. String-based exceptions began issuing deprecation warnings in 2.5 and were removed in 2.6 and 3.0, so today you should use class-based exceptions, as shown in this book. If you work with legacy code, though, you might still come across string exceptions. They might also appear in books, tutorials, and web resources written a few years ago (which qualifies as an eternity in Python years!).

String exceptions were straightforward to use—any string would do, and they matched by object identity, not value (that is, using is, not ==):

C:\code> C:\Python25\python

>>> myexc = "My exception string" # Were we ever this young?...

>>> try:

... raise myexc

... except myexc:

... print('caught')

...

caught

This form of exception was removed because it was not as good as classes for larger programs and code maintenance. In modern Pythons, string exceptions trigger exceptions instead:

C:\code> py −3

>>> raise 'spam'

TypeError: exceptions must derive from BaseException

C:\code> py −2

>>> raise 'spam'

TypeError: exceptions must be old-style classes or derived from BaseException, ...etc

Although you can’t use string exceptions today, they actually provide a natural vehicle for introducing the class-based exceptions model.

Class-Based Exceptions

Strings were a simple way to define exceptions. As described earlier, however, classes have some added advantages that merit a quick look. Most prominently, they allow us to identify exception categories that are more flexible to use and maintain than simple strings. Moreover, classes naturally allow for attached exception details and support inheritance. Because they are seen by many as the better approach, they are now required.

Coding details aside, the chief difference between string and class exceptions has to do with the way that exceptions raised are matched against except clauses in try statements:

§ String exceptions were matched by simple object identity: the raised exception was matched to except clauses by Python’s is test.

§ Class exceptions are matched by superclass relationships: the raised exception matches an except clause if that except clause names the exception instance’s class or any superclass of it.

That is, when a try statement’s except clause lists a superclass, it catches instances of that superclass, as well as instances of all its subclasses lower in the class tree. The net effect is that class exceptions naturally support the construction of exception hierarchies: superclasses become category names, and subclasses become specific kinds of exceptions within a category. By naming a general exception superclass, an except clause can catch an entire category of exceptions—any more specific subclass will match.

String exceptions had no such concept: because they were matched by simple object identity, there was no direct way to organize exceptions into more flexible categories or groups. The net result was that exception handlers were coupled with exception sets in a way that made changes difficult.

In addition to this category idea, class-based exceptions better support exception state information (attached to instances) and allow exceptions to participate in inheritance hierarchies (to obtain common behaviors). Because they offer all the benefits of classes and OOP in general, they provide a more powerful alternative to the now-defunct string-based exceptions model in exchange for a small amount of additional code.

Coding Exceptions Classes

Let’s look at an example to see how class exceptions translate to code. In the following file, classexc.py, we define a superclass called General and two subclasses called Specific1 and Specific2. This example illustrates the notion of exception categories—General is a category name, and its two subclasses are specific types of exceptions within the category. Handlers that catch General will also catch any subclasses of it, including Specific1 and Specific2:

class General(Exception): pass

class Specific1(General): pass

class Specific2(General): pass

def raiser0():

X = General() # Raise superclass instance

raise X

def raiser1():

X = Specific1() # Raise subclass instance

raise X

def raiser2():

X = Specific2() # Raise different subclass instance

raise X

for func in (raiser0, raiser1, raiser2):

try:

func()

except General: # Match General or any subclass of it

import sys

print('caught: %s' % sys.exc_info()[0])

C:\code> python classexc.py

caught: <class '__main__.General'>

caught: <class '__main__.Specific1'>

caught: <class '__main__.Specific2'>

This code is mostly straightforward, but here are a few points to notice:

Exception superclass

Classes used to build exception category trees have very few requirements—in fact, in this example they are mostly empty, with bodies that do nothing but pass. Notice, though, how the top-level class here inherits from the built-in Exception class. This is required in Python 3.X; Python 2.X allows standalone classic classes to serve as exceptions too, but it requires new-style classes to be derived from built-in exception classes just as in 3.X. Although we don’t employ it here, because Exception provides some useful behavior we’ll meet later, it’s a good idea to inherit from it in either Python.

Raising instances

In this code, we call classes to make instances for the raise statements. In the class exception model, we always raise and catch a class instance object. If we list a class name without parentheses in a raise, Python calls the class with no constructor argument to make an instance for us. Exception instances can be created before the raise, as done here, or within the raise statement itself.

Catching categories

This code includes functions that raise instances of all three of our classes as exceptions, as well as a top-level try that calls the functions and catches General exceptions. The same try also catches the two specific exceptions, because they are subclasses of General—members of its category.

Exception details

The exception handler here uses the sys.exc_info call—as we’ll see in more detail in the next chapter, it’s how we can grab hold of the most recently raised exception in a generic fashion. Briefly, the first item in its result is the class of the exception raised, and the second is the actual instance raised. In a general except clause like the one here that catches all classes in a category, sys.exc_info is one way to determine exactly what’s occurred. In this particular case, it’s equivalent to fetching the instance’s __class__ attribute. As we’ll see in the next chapter, thesys.exc_info scheme is also commonly used with empty except clauses that catch everything.

The last point merits further explanation. When an exception is caught, we can be sure that the instance raised is an instance of the class listed in the except, or one of its more specific subclasses. Because of this, the __class__ attribute of the instance also gives the exception type. The following variant in classexc2.py, for example, works the same as the prior example—it uses the as extension in its except clause to assign a variable to the instance actually raised:

class General(Exception): pass

class Specific1(General): pass

class Specific2(General): pass

def raiser0(): raise General()

def raiser1(): raise Specific1()

def raiser2(): raise Specific2()

for func in (raiser0, raiser1, raiser2):

try:

func()

except General as X: # X is the raised instance

print('caught: %s' % X.__class__) # Same as sys.exc_info()[0]

Because __class__ can be used like this to determine the specific type of exception raised, sys.exc_info is more useful for empty except clauses that do not otherwise have a way to access the instance or its class. Furthermore, more realistic programs usually should not have to careabout which specific exception was raised at all—by calling methods of the exception class instance generically, we automatically dispatch to behavior tailored for the exception raised.

More on this and sys.exc_info in the next chapter; also see Chapter 29 and Part VI at large if you’ve forgotten what __class__ means in an instance, and the prior chapter for a review of the as used here.

Why Exception Hierarchies?

Because there are only three possible exceptions in the prior section’s example, it doesn’t really do justice to the utility of class exceptions. In fact, we could achieve the same effects by coding a list of exception names in parentheses within the except clause:

try:

func()

except (General, Specific1, Specific2): # Catch any of these

...

This approach worked for the defunct string exception model too. For large or high exception hierarchies, however, it may be easier to catch categories using class-based categories than to list every member of a category in a single except clause. Perhaps more importantly, you can extend exception hierarchies as software needs evolve by adding new subclasses without breaking existing code.

Suppose, for example, you code a numeric programming library in Python, to be used by a large number of people. While you are writing your library, you identify two things that can go wrong with numbers in your code—division by zero, and numeric overflow. You document these as the two standalone exceptions that your library may raise:

# mathlib.py

class Divzero(Exception): pass

class Oflow(Exception): pass

def func():

...

raise Divzero()

...and so on...

Now, when people use your library, they typically wrap calls to your functions or classes in try statements that catch your two exceptions; after all, if they do not catch your exceptions, exceptions from your library will kill their code:

# client.py

import mathlib

try:

mathlib.func(...)

except (mathlib.Divzero, mathlib.Oflow):

...handle and recover...

This works fine, and lots of people start using your library. Six months down the road, though, you revise it (as programmers are prone to do!). Along the way, you identify a new thing that can go wrong—underflow, perhaps—and add that as a new exception:

# mathlib.py

class Divzero(Exception): pass

class Oflow(Exception): pass

class Uflow(Exception): pass

Unfortunately, when you re-release your code, you create a maintenance problem for your users. If they’ve listed your exceptions explicitly, they now have to go back and change every place they call your library to include the newly added exception name:

# client.py

try:

mathlib.func(...)

except (mathlib.Divzero, mathlib.Oflow, mathlib.Uflow):

...handle and recover...

This may not be the end of the world. If your library is used only in-house, you can make the changes yourself. You might also ship a Python script that tries to fix such code automatically (it would probably be only a few dozen lines, and it would guess right at least some of the time). If many people have to change all their try statements each time you alter your exception set, though, this is not exactly the most polite of upgrade policies.

Your users might try to avoid this pitfall by coding empty except clauses to catch all possible exceptions:

# client.py

try:

mathlib.func(...)

except: # Catch everything here (or catch Exception super)

...handle and recover...

But this workaround might catch more than they bargained for—things like running out of memory, keyboard interrupts (Ctrl-C), system exits, and even typos in their own try block’s code will all trigger exceptions, and such things should pass, not be caught and erroneously classified as library errors. Catching the Exception super class improves on this, but still intercepts—and thus may mask—program errors.

And really, in this scenario users want to catch and recover from only the specific exceptions the library is defined and documented to raise. If any other exception occurs during a library call, it’s likely a genuine bug in the library (and probably time to contact the vendor!). As a rule of thumb, it’s usually better to be specific than general in exception handlers—an idea we’ll revisit as a “gotcha” in the next chapter.[70]

So what to do, then? Class exception hierarchies fix this dilemma completely. Rather than defining your library’s exceptions as a set of autonomous classes, arrange them into a class tree with a common superclass to encompass the entire category:

# mathlib.py

class NumErr(Exception): pass

class Divzero(NumErr): pass

class Oflow(NumErr): pass

def func():

...

raise DivZero()

...and so on...

This way, users of your library simply need to list the common superclass (i.e., category) to catch all of your library’s exceptions, both now and in the future:

# client.py

import mathlib

try:

mathlib.func(...)

except mathlib.NumErr:

...report and recover...

When you go back and hack (update) your code again, you can add new exceptions as new subclasses of the common superclass:

# mathlib.py

...

class Uflow(NumErr): pass

The end result is that user code that catches your library’s exceptions will keep working, unchanged. In fact, you are free to add, delete, and change exceptions arbitrarily in the future—as long as clients name the superclass, and that superclass remains intact, they are insulated from changes in your exceptions set. In other words, class exceptions provide a better answer to maintenance issues than strings could.

Class-based exception hierarchies also support state retention and inheritance in ways that make them ideal in larger programs. To understand these roles, though, we first need to see how user-defined exception classes relate to the built-in exceptions from which they inherit.


[70] As a clever student of mine suggested, the library module could also provide a tuple object that contains all the exceptions the library can possibly raise—the client could then import the tuple and name it in an except clause to catch all the library’s exceptions (recall that including a tuple in an except means catch any of its exceptions). When new exceptions are added later, the library can just expand the exported tuple. This would work, but you’d still need to keep the tuple up-to-date with raised exceptions inside the library module. Also, class hierarchies offer more benefits than just categories—they also support inherited state and methods and a customization model that individual exceptions do not.

Built-in Exception Classes

I didn’t really pull the prior section’s examples out of thin air. All built-in exceptions that Python itself may raise are predefined class objects. Moreover, they are organized into a shallow hierarchy with general superclass categories and specific subclass types, much like the prior section’s exceptions class tree.

In Python 3.X, all the familiar exceptions you’ve seen (e.g., SyntaxError) are really just predefined classes, available as built-in names in the module named builtins; in Python 2.X, they instead live in __builtin__ and are also attributes of the standard library module exceptions. In addition, Python organizes the built-in exceptions into a hierarchy, to support a variety of catching modes. For example:

BaseException: topmost root, printing and constructor defaults

The top-level root superclass of exceptions. This class is not supposed to be directly inherited by user-defined classes (use Exception instead). It provides default printing and state retention behavior inherited by subclasses. If the str built-in is called on an instance of this class (e.g., byprint), the class returns the display strings of the constructor arguments passed when the instance was created (or an empty string if there were no arguments). In addition, unless subclasses replace this class’s constructor, all of the arguments passed to this class at instance construction time are stored in its args attribute as a tuple.

Exception: root of user-defined exceptions

The top-level root superclass of application-related exceptions. This is an immediate subclass of BaseException and is a superclass to every other built-in exception, except the system exit event classes (SystemExit, KeyboardInterrupt, and GeneratorExit). Nearly all user-defined classes should inherit from this class, not BaseException. When this convention is followed, naming Exception in a try statement’s handler ensures that your program will catch everything but system exit events, which should normally be allowed to pass. In effect,Exception becomes a catchall in try statements and is more accurate than an empty except.

ArithmeticError: root of numeric errors

A subclass of Exception, and the superclass of all numeric errors. Its subclasses identify specific numeric errors: OverflowError, ZeroDivisionError, and FloatingPointError.

LookupError: root of indexing errors

A subclass of Exception, and the superclass category for indexing errors for both sequences and mappings—IndexError and KeyError—as well as some Unicode lookup errors.

And so on—because the built-in exception set is prone to frequent changes, this book doesn’t document it exhaustively. You can read further about this structure in reference texts such as Python Pocket Reference or the Python library manual. In fact, the exceptions class tree differs slightly between Python 3.X and 2.X in ways we’ll omit here, because they are not relevant to examples.

You can also see the built-in exceptions class tree in the help text of the exceptions module in Python 2.X only (see Chapter 4 and Chapter 15 for help on help):

>>> import exceptions

>>> help(exceptions)

...lots of text omitted...

This module is removed in 3.X, where you’ll find up-to-date help in the other resources mentioned.

Built-in Exception Categories

The built-in class tree allows you to choose how specific or general your handlers will be. For example, because the built-in exception ArithmeticError is a superclass for more specific exceptions such as OverflowError and ZeroDivisionError:

§ By listing ArithmeticError in a try, you will catch any kind of numeric error raised.

§ By listing ZeroDivisionError, you will intercept just that specific type of error, and no others.

Similarly, because Exception is the superclass of all application-level exceptions in Python 3.X, you can generally use it as a catchall—the effect is much like an empty except, but it allows system exit exceptions to pass and propagate as they usually should:

try:

action()

except Exception: # Exits not caught here

...handle all application exceptions...

else:

...handle no-exception case...

This doesn’t quite work universally in Python 2.X, however, because standalone user-defined exceptions coded as classic classes are not required to be subclasses of the Exception root class. This technique is more reliable in Python 3.X, since it requires all classes to derive from built-in exceptions. Even in Python 3.X, though, this scheme suffers most of the same potential pitfalls as the empty except, as described in the prior chapter—it might intercept exceptions intended for elsewhere, and it might mask genuine programming errors. Since this is such a common issue, we’ll revisit it as a “gotcha” in the next chapter.

Whether or not you will leverage the categories in the built-in class tree, it serves as a good example; by using similar techniques for class exceptions in your own code, you can provide exception sets that are flexible and easily modified.

NOTE

Python 3.3 reworks the built-in IO and OS exception hierarchies. It adds new specific exception classes corresponding to common file and system error numbers, and groups these and others related to operating system calls under the OSError category superclass. Former exception names are retained for backward compatibility.

Prior to this, programs inspect the data attached to the exception instance to see what specific error occurred, and possibly reraise others to be propagated (the errno module has names preset to the error codes for convenience, and the error number is available in both the generic tuple as V.args[0] and attribute V.errno):

c:\temp> py −3.2

>>> try:

... f = open('nonesuch.txt')

... except IOError as V:

... if V.errno == 2: # Or errno.N, V.args[0]

... print('No such file')

... else:

... raise # Propagate others

...

No such file

This code still works in 3.3, but with the new classes, programs in 3.3 and later can be more specific about the exceptions they mean to process, and ignore others:

c:\temp> py −3.3

>>> try:

... f = open('nonesuch.txt')

... except FileNotFoundError:

... print('No such file')

...

No such file

For full details on this extension and its classes, see the other resources listed earlier.

Default Printing and State

Built-in exceptions also provide default print displays and state retention, which is often as much logic as user-defined classes require. Unless you redefine the constructors your classes inherit from them, any constructor arguments you pass to these classes are automatically saved in the instance’s args tuple attribute, and are automatically displayed when the instance is printed. An empty tuple and display string are used if no constructor arguments are passed, and a single argument displays as itself (not as a tuple).

This explains why arguments passed to built-in exception classes show up in error messages—any constructor arguments are attached to the instance and displayed when the instance is printed:

>>> raise IndexError # Same as IndexError(): no arguments

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

IndexError

>>> raise IndexError('spam') # Constructor argument attached, printed

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

IndexError: spam

>>> I = IndexError('spam') # Available in object attribute

>>> I.args

('spam',)

>>> print(I) # Displays args when printed manually

spam

The same holds true for user-defined exceptions in Python 3.X (and for new-style classes in 2.X), because they inherit the constructor and display methods present in their built-in superclasses:

>>> class E(Exception): pass

...

>>> raise E

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

__main__.E

>>> raise E('spam')

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

__main__.E: spam

>>> I = E('spam')

>>> I.args

('spam',)

>>> print(I)

spam

When intercepted in a try statement, the exception instance object gives access to both the original constructor arguments and the display method:

>>> try:

... raise E('spam')

... except E as X:

... print(X) # Displays and saves constructor arguments

... print(X.args)

... print(repr(X))

...

spam

('spam',)

E('spam',)

>>> try: # Multiple arguments save/display a tuple

... raise E('spam', 'eggs', 'ham')

... except E as X:

... print('%s %s' % (X, X.args))

...

('spam', 'eggs', 'ham') ('spam', 'eggs', 'ham')

Note that exception instance objects are not strings themselves, but use the __str__ operator overloading protocol we studied in Chapter 30 to provide display strings when printed; to concatenate with real strings, perform manual conversions: str(X) + 'astr', '%s' % X, and the like.

Although this automatic state and display support is useful by itself, for more specific display and state retention needs you can always redefine inherited methods such as __str__ and __init__ in Exception subclasses—as the next section shows.

Custom Print Displays

As we saw in the preceding section, by default, instances of class-based exceptions display whatever you passed to the class constructor when they are caught and printed:

>>> class MyBad(Exception): pass

...

>>> try:

... raise MyBad('Sorry--my mistake!')

... except MyBad as X:

... print(X)

...

Sorry--my mistake!

This inherited default display model is also used if the exception is displayed as part of an error message when the exception is not caught:

>>> raise MyBad('Sorry--my mistake!')

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

__main__.MyBad: Sorry--my mistake!

For many roles, this is sufficient. To provide a more custom display, though, you can define one of two string-representation overloading methods in your class (__repr__ or __str__) to return the string you want to display for your exception. The string the method returns will be displayed if the exception either is caught and printed or reaches the default handler:

>>> class MyBad(Exception):

... def __str__(self):

... return 'Always look on the bright side of life...'

...

>>> try:

... raise MyBad()

... except MyBad as X:

... print(X)

...

Always look on the bright side of life...

>>> raise MyBad()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

__main__.MyBad: Always look on the bright side of life...

Whatever your method returns is included in error messages for uncaught exceptions and used when exceptions are printed explicitly. The method returns a hardcoded string here to illustrate, but it can also perform arbitrary text processing, possibly using state information attached to the instance object. The next section looks at state information options.

NOTE

A subtle point here: you generally must redefine __str__ for exception display purposes, because the built-in exception superclasses already have a __str__ method, and __str__ is preferred to __repr__ in some contexts—including error message displays. If you define a __repr__, printing will happily call the built-in superclass’s __str__ instead!

>>> class E(Exception):

def __repr__(self): return 'Not called!'

>>> raise E('spam')

...

__main__.E: spam

>>> class E(Exception):

def __str__(self): return 'Called!'

>>> raise E('spam')

...

__main__.E: Called!

See Chapter 30 for more details on these special operator overloading methods.

Custom Data and Behavior

Besides supporting flexible hierarchies, exception classes also provide storage for extra state information as instance attributes. As we saw earlier, built-in exception superclasses provide a default constructor that automatically saves constructor arguments in an instance tuple attribute namedargs. Although the default constructor is adequate for many cases, for more custom needs we can provide a constructor of our own. In addition, classes may define methods for use in handlers that provide precoded exception processing logic.

Providing Exception Details

When an exception is raised, it may cross arbitrary file boundaries—the raise statement that triggers an exception and the try statement that catches it may be in completely different module files. It is not generally feasible to store extra details in global variables because the try statement might not know which file the globals reside in. Passing extra state information along in the exception itself allows the try statement to access it more reliably.

With classes, this is nearly automatic. As we’ve seen, when an exception is raised, Python passes the class instance object along with the exception. Code in try statements can access the raised instance by listing an extra variable after the as keyword in an except handler. This provides a natural hook for supplying data and behavior to the handler.

For example, a program that parses data files might signal a formatting error by raising an exception instance that is filled out with extra details about the error:

>>> class FormatError(Exception):

def __init__(self, line, file):

self.line = line

self.file = file

>>> def parser():

raise FormatError(42, file='spam.txt') # When error found

>>> try:

... parser()

... except FormatError as X:

... print('Error at: %s %s' % (X.file, X.line))

...

Error at: spam.txt 42

In the except clause here, the variable X is assigned a reference to the instance that was generated when the exception was raised. This gives access to the attributes attached to the instance by the custom constructor. Although we could rely on the default state retention of built-in superclasses, it’s less relevant to our application (and doesn’t support the keyword arguments used in the prior example):

>>> class FormatError(Exception): pass # Inherited constructor

>>> def parser():

raise FormatError(42, 'spam.txt') # No keywords allowed!

>>> try:

... parser()

... except FormatError as X:

... print('Error at:', X.args[0], X.args[1]) # Not specific to this app

...

Error at: 42 spam.txt

Providing Exception Methods

Besides enabling application-specific state information, custom constructors also better support extra behavior for exception objects. That is, the exception class can also define methods to be called in the handler. The following code in excparse.py, for example, adds a method that uses exception state information to log errors to a file automatically:

from __future__ import print_function # 2.X compatibility

class FormatError(Exception):

logfile = 'formaterror.txt'

def __init__(self, line, file):

self.line = line

self.file = file

def logerror(self):

log = open(self.logfile, 'a')

print('Error at:', self.file, self.line, file=log)

def parser():

raise FormatError(40, 'spam.txt')

if __name__ == '__main__':

try:

parser()

except FormatError as exc:

exc.logerror()

When run, this script writes its error message to a file in response to method calls in the exception handler:

c:\code> del formaterror.txt

c:\code> py −3 excparse.py

c:\code> py −2 excparse.py

c:\code> type formaterror.txt

Error at: spam.txt 40

Error at: spam.txt 40

In such a class, methods (like logerror) may also be inherited from superclasses, and instance attributes (like line and file) provide a place to save state information that provides extra context for use in later method calls. Moreover, exception classes are free to customize and extend inherited 'margin-top:7.5pt;margin-right:0cm;margin-bottom:7.5pt; margin-left:20.0pt;line-height:normal;vertical-align:baseline'>class CustomFormatError(FormatError):

def logerror(self):

...something unique here...

raise CustomFormatError(...)

In other words, because they are defined with classes, all the benefits of OOP that we studied in Part VI are available for use with exceptions in Python.

Two final notes here: first, the raised instance object assigned to exc in this code is also available generically as the second item in the result tuple of the sys.exc_info() call—a tool that returns information about the most recently raised exception. This interface must be used if you do not list an exception name in an except clause but still need access to the exception that occurred, or to any of its attached state information or methods. Second, although our class’s logerror method appends a custom message to a logfile, it could also generate Python’s standard error message with stack trace using tools in the traceback standard library module, which uses traceback objects.

To learn more about sys.exc_info and tracebacks, though, we need to move ahead to the next chapter.

Chapter Summary

In this chapter, we explored coding user-defined exceptions. As we learned, exceptions are implemented as class instance objects as of Python 2.6 and 3.0 (an earlier string-based exception model alternative was available in earlier releases but has now been deprecated). Exception classes support the concept of exception hierarchies that ease maintenance, allow data and behavior to be attached to exceptions as instance attributes and methods, and allow exceptions to inherit data and behavior from superclasses.

We saw that in a try statement, catching a superclass catches that class as well as all subclasses below it in the class tree—superclasses become exception category names, and subclasses become more specific exception types within those categories. We also saw that the built-in exception superclasses we must inherit from provide usable defaults for printing and state retention, which we can override if desired.

The next chapter wraps up this part of the book by exploring some common use cases for exceptions and surveying tools commonly used by Python programmers. Before we get there, though, here’s this chapter’s quiz.

Test Your Knowledge: Quiz

1. What are the two new constraints on user-defined exceptions in Python 3.X?

2. How are raised class-based exceptions matched to handlers?

3. Name two ways that you can attach context information to exception objects.

4. Name two ways that you can specify the error message text for exception objects.

5. Why should you not use string-based exceptions anymore today?

Test Your Knowledge: Answers

1. In 3.X, exceptions must be defined by classes (that is, a class instance object is raised and caught). In addition, exception classes must be derived from the built-in class BaseException; most programs inherit from its Exception subclass, to support catchall handlers for normal kinds of exceptions.

2. Class-based exceptions match by superclass relationships: naming a superclass in an exception handler will catch instances of that class, as well as instances of any of its subclasses lower in the class tree. Because of this, you can think of superclasses as general exception categories and subclasses as more specific types of exceptions within those categories.

3. You can attach context information to class-based exceptions by filling out instance attributes in the instance object raised, usually in a custom class constructor. For simpler needs, built-in exception superclasses provide a constructor that stores its arguments on the instance automatically (as a tuple in the attribute args). In exception handlers, you list a variable to be assigned to the raised instance, then go through this name to access attached state information and call any methods defined in the class.

4. The error message text in class-based exceptions can be specified with a custom __str__ operator overloading method. For simpler needs, built-in exception superclasses automatically display anything you pass to the class constructor. Operations like print and str automatically fetch the display string of an exception object when it is printed either explicitly or as part of an error message.

5. Because Guido said so—they have been removed as of both Python 2.6 and 3.0. There are arguably good reasons for this: string-based exceptions did not support categories, state information, or behavior inheritance in the way class-based exceptions do. In practice, this made string-based exceptions easier to use at first when programs were small, but more complex to use as programs grew larger.

The downsides of requiring exceptions to be classes are to break existing code, and create a forward knowledge dependency—beginners must first learn classes and OOP before they can code new exceptions, or even truly understand exceptions at all. In fact, this is why this relatively straightforward topic was largely postponed until this point in the book. For better or worse, such dependencies are not uncommon in Python today (see the preface and conclusion for more on such things).