-
Notifications
You must be signed in to change notification settings - Fork 222
/
item_51_define_a_root_exception.py
154 lines (116 loc) · 5.9 KB
/
item_51_define_a_root_exception.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# Item 51: Define a root exception to insulate callers from APIs
# When you're defining a module's API, the exceptions you throw are just as
# much a part of your interface as the functions and classes you define (see
# Item 14: "Prefer exceptions to returning None").
# Python has a built-in hierarchy of exceptions for the language and standard
# library. There's a draw to using the built-in exception types for reporting
# errors instead of defining your own new types. For example, you could raise
# a ValueError exception whenever an invalid parameter is passed to your
# function.
def determine_weight(volume, density):
if density < 0:
raise ValueError('Density must be positive')
# ...
# In some cases, using ValueError makes sense, but for APIs it's much more
# powerful to define your own hierarchy of exceptions. You can do this by
# providing a root Exception in your module. Then, have all other exceptions
# raised by that module inherit from the root exception.from
# my_module.py
class Error(Exception):
"""Base-class for all exceptions raised by this module."""
pass
class InvalidDensityError(Error):
"""There was a problem with a provided density value."""
pass
# Having a root exception in a module makes it easy for consumers of your API
# to catch all of the exceptions that you raise on purpose. For example, here
# a consumer of your API makes a function all with a try/except statement that
# catches your root exception:
# try:
# weight = my_module.determine_weight(1, -1)
# except my_module.Error as e:
# logging.error('Unexpected error: %s', e)
# The try/except prevents your API's exceptions from progagating too far
# upward and breaking the calling program. It insulates the calling code from
# your API. This insulation has three helpful effects.
# First, root exceptions let callers understand when there's a problem with
# their usage of your API. If callers are using your API properly, they should
# catch the various exceptions that you deliberately raise. If they don't
# handle such an exception, it will propagate all the way up to the insulating
# except block that catches your module's root exception. That block can bring
# the exception to the attention of the API consumer, giving them a chance to
# add proper handling of the exception type.
# try:
# weight = my_module.determine_weight(1, -1)
# except my_module.InvalidDensityError:
# weight = 0
# except my_module.Error as e:
# logging.error('Bug in the calling code: %s', e)
# The second advantage of using root exceptions is that they can help find
# bugs in your API module's code. If your code only deliberately raises
# exceptions that you define within your module's hierarchy, then all other
# types of exceptions raised by your module must be the ones that you didn't
# intend to raise. These are bugs in your API's code.
# Using the try/except statement above will not insulate API consumers from
# bugs in your API module's code. To do that, the caller need to add another
# except block that catches Python's base Exception class. This allows the
# API consumer to detect when there's a bug in the API module's implementation
# that needs to be fixed.
# try:
# weight = my_module.determine_weight(1, -1)
# except my_module.InvalidDensityError:
# weight = 0
# except my_module.Error as e:
# logging.error('Bug in the calling code: %s', e)
# except Exception as e:
# logging.error('Bug in the API code: %s', e)
# The third impact of using root exceptions is future-proofing your API. Over
# time, you may want to expand your API to provide more specific exceptions in
# certain situation. For example, you could add an Exception subclass that
# indicates the error condition of supplying negative densities.
# my_module.py
class NegativeDensityError(InvalidDensityError):
"""A provided density value was negative."""
pass
def determine_weight(volume, density):
if density < 0:
raise NegativeDensityError
# The calling code will continue to work exactly as before because it already
# catches InvalidDensityError exceptions (the parent class of
# NegativeDensityError). In the future, the caller could decide to
# special-case the new type of exception and change its behavior accordingly.
# try:
# weight = my_module.determine_weight(1, -1)
# except my_module.NegativeDensityError as e:
# raise ValueError('Must supply non-negative density') from e
# except my_module.InvalidDensityError:
# weight = 0
# except my_module.Error as e:
# logging.error('Bug in the calling code: %s', e)
# except Exception as e:
# logging.error('Bug in the API code: %s', e)
# You can take API future-proofing further by providing a broader set of
# exceptions directly below the root exception. For example, imagine you had
# one set of errors related to calculating weights, another related to
# calculating volume, and a third related to calculating density.
# my_module.py
class WeightError(Error):
"""Base-class for weight calculation errors."""
class VolumeError(Error):
"""Base-class for volume calculation errors."""
class DensityError(Error):
"""Base-class for density calculation errors."""
# Specific exceptions would inherit from these general exceptions. Each
# intermediate exception acts as its own kind of root exception. This makes
# it easier to insulate layers of calling code from API code based on broad
# functionality. This is much better than having all callers catch a long
# list of very specific Exception subclasses.
# Things to remember
# 1. Defining root exceptions for your modules allows API consumers to
# insulate themselves from your API.
# 2. Catching root exceptions can help you find bugs in code that consumes an
# API.
# 3. Catching the Python Exception base class can help you find bugs in API
# implementations.
# 4. Intermediate root exceptions let you add more specific types of
# exceptions in the future without breaking your API consumers.