You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
If you wish, you can shorten the amount of information returned in the error by adding `from None` when you raise the error. This will look nicer to a user, but you lose some detail in the error traceback.
102
+
```{tip} Informative Errors
103
+
Notice we included the value that caused the error in the `IndexError` message,
104
+
and a suggestion about what we expected!
105
105
106
-
```{code-cell} ipython3
107
-
---
108
-
editable: true
109
-
slideshow:
110
-
slide_type: ''
111
-
tags: [raises-exception]
112
-
---
113
-
def clean_title(title):
114
-
"""
115
-
Attempts to return the first character of the title.
116
-
Raises the same error with a friendly message if the input is invalid.
117
-
"""
118
-
try:
119
-
return title[0]
120
-
except IndexError as e:
121
-
raise IndexError(f"Oops! You provided a title in an unexpected format. "
122
-
f"I expected the title to be provided in a list and you provided "
123
-
f"a {type(title)}.") from None
124
-
125
-
# Run the function
126
-
print(clean_title(""))
106
+
Include enough detail in your exceptions so that the person reading them knows
107
+
why the error occurred, and ideally what they should do about it.
@@ -138,7 +119,7 @@ Here’s how try/except blocks work:
138
119
***try block:** You write the code that might cause an error here. Python will attempt to run this code.
139
120
***except block:** If Python encounters an error in the try block, it jumps to the except block to handle it. You can specify what to do when an error occurs, such as printing a friendly message or providing a fallback option.
140
121
141
-
A `try/except` block looks like this:
122
+
A `try/except` block looks like this[^more_forms]:
You could anticipate a user providing a bad file path. This might be especailly possible if you plan to share your code with others and run it on different computers and different operating systems.
197
+
You could anticipate a user providing a bad file path. This might be especially possible if you plan to share your code with others and run it on different computers and different operating systems.
217
198
218
199
In the example below, you use a [conditional statement](conditionals) to check if the file exists; if it doesn't, it returns None. In this case, the code will fail quietly, and the user will not understand that there is an error.
219
200
@@ -227,7 +208,7 @@ slideshow:
227
208
---
228
209
import os
229
210
230
-
def read_file(file_path):
211
+
def read_file_silent(file_path):
231
212
if os.path.exists(file_path):
232
213
with open(file_path, 'r') as file:
233
214
data = file.read()
@@ -236,24 +217,107 @@ def read_file(file_path):
236
217
return None # Doesn't fail immediately, just returns None
237
218
238
219
# No error raised, even though the file doesn't exist
This code example below is better than the examples above for three reasons:
225
+
Even if you know that it is possible for a `FileNotFoundFoundError` to be raised here, it's better to raise the exception rather than catch it and proceed silently so the person calling the function knows there is a problem they need to address.
245
226
246
-
1. It's **pythonic**: it asks for forgiveness later by using a try/except
247
-
2. It fails quickly - as soon as it tries to open the file. The code won't continue to run after this step fails.
248
-
3. It raises a clean, useful error that the user can understand
227
+
Say for example reading the data was one step in a longer chain of analyses with other steps that take a long time in between when the data was loaded and when it was used:
The code anticipates what will happen if it can't find the file. It then raises a `FileNotFoundError` and provides a useful and friendly message to the user.
236
+
# And this simulation runs overnight...
237
+
generated_data = expensive_simulation()
238
+
239
+
# We'll only realize there is a problem here,
240
+
# and by then the problem might not be obvious!
241
+
analyze_data(data, big_file, generated_data)
242
+
```
243
+
244
+
By silencing the error, we wasted our whole day!
245
+
246
+
## Catching and Using Exceptions
247
+
248
+
If we want to raise exceptions as soon as they happen,
249
+
why would we ever want to catch them with `try`/`catch`?
250
+
Catching exceptions allows us to choose how we react to them -
251
+
and vice versa when someone else is running our code,
252
+
raising exceptions lets them know there is a problem and gives them the opportunity to decide how to proceed.
253
+
254
+
For example, you might have many datasets to process,
255
+
and you don't want to waste time processing one that has missing data,
256
+
but you also don't want to stop the whole run because an exception happens
257
+
somewhere deep within the nest of code.
258
+
The *combination* of failing fast with error handling allows us to do that!
259
+
260
+
Here we use the `except {exception type} as {variable}` syntax to *use* the error after catching it,
261
+
and we store error messages for each dataset to analyze and display them at the end:
262
+
263
+
```{code-cell} ipython3
264
+
from rich.pretty import pprint
265
+
266
+
data = [2, 4, 6, 8, 'uh oh']
267
+
268
+
def divide_by_two(value):
269
+
return value/2
270
+
271
+
def my_analysis(data):
272
+
results = {}
273
+
errors = {}
274
+
for value in data:
275
+
try:
276
+
results[value] = divide_by_two(value)
277
+
except TypeError as e:
278
+
errors[value] = str(e)
279
+
return {'results': results, 'errors': errors}
280
+
281
+
results = my_analysis(data)
282
+
pprint(results, expand_all=False)
283
+
```
284
+
285
+
These techniques stack! So add one more level where we imagine someone else is using our analysis code. They might want to raise an exception to stop processing the rest of their data.
286
+
287
+
```{code-cell} ipython3
288
+
---
289
+
tags: [raises-exception]
290
+
---
291
+
292
+
def someone_elses_analysis(data):
293
+
processed = my_analysis(data)
294
+
if processed['errors']:
295
+
raise RuntimeError(f"Caught exception from my_analysis: {processed['errors']}")
296
+
297
+
someone_elses_analysis(data)
298
+
```
299
+
300
+
301
+
## Customizing error messages
302
+
303
+
Recall the exception from our missing file:
304
+
305
+
```{code-cell} ipython3
306
+
---
307
+
tags: [raises-exception]
308
+
---
309
+
310
+
file_data = read_file("nonexistent_file.txt")
311
+
```
312
+
313
+
### Focusing Information - Raising New Exceptions
314
+
315
+
The error is useful because it fails and provides a simple and effective message that tells the user to check that their file path is correct. But there's a lot of information there! The traceback shows us each line of code in between the place where you called the function and where the exception was raised: the `read_file()` call, the `open()` call, the bottom-level `IPython` exception. If you wanted to provide less information to the user, you could catch it and raise a *new* exception.
316
+
317
+
If you simply raise a new exception, it is [chained](https://docs.python.org/3/tutorial/errors.html#exception-chaining) to the previous error, which is noisier, not tidier!
251
318
252
319
```{code-cell} ipython3
253
320
---
254
-
editable: true
255
-
slideshow:
256
-
slide_type: ''
257
321
tags: [raises-exception]
258
322
---
259
323
def read_file(file_path):
@@ -262,20 +326,17 @@ def read_file(file_path):
262
326
data = file.read()
263
327
return data
264
328
except FileNotFoundError:
265
-
raise FileNotFoundError(f"Oops! I couldn't find the file located at: {file_path}. Please check to see if it exists.")
329
+
raise FileNotFoundError(
330
+
f"Oops! I couldn't find the file located at: {file_path}. "
331
+
"Please check to see if it exists."
332
+
) # no "from" statement implicitly chains the prior error
333
+
266
334
267
-
# Raises an error immediately if the file doesn't exist
268
335
file_data = read_file("nonexistent_file.txt")
269
336
```
270
337
271
-
## Customizing error messages
338
+
Instead we can use the exception chaining syntax, `raise {exception} from {other exception}`, to explicitly exclude the original error from the traceback.
272
339
273
-
The code above is useful because it fails and provides a simple and effective message that tells the user to check that their file path is correct.
274
-
275
-
However, the amount of text returned from the error is significant because it finds the error when it can't open the file. Still, then you raise the error intentionally within the except statement.
276
-
277
-
If you wanted to provide less information to the user, you could use `from None`. From None ensure that you
278
-
only return the exception information related to the error that you handle within the try/except block.
279
340
280
341
```{code-cell} ipython3
281
342
---
@@ -287,14 +348,52 @@ def read_file(file_path):
287
348
data = file.read()
288
349
return data
289
350
except FileNotFoundError:
290
-
raise FileNotFoundError(f"Oops! I couldn't find the file located at: {file_path}. Please check to see if it exists.") from None
351
+
raise FileNotFoundError(
352
+
f"Oops! I couldn't find the file located at: {file_path}. "
353
+
"Please check to see if it exists."
354
+
) from None # explicitly break the exception chain
355
+
291
356
292
-
# Raises an error immediately if the file doesn't exist
293
357
file_data = read_file("nonexistent_file.txt")
294
358
```
295
359
360
+
This code example below is better than the examples above for three reasons:
361
+
362
+
1. It's **pythonic**: it asks for forgiveness later by using a try/except
363
+
2. It fails quickly - as soon as it tries to open the file. The code won't continue to run after this step fails.
364
+
3. It raises a clean, useful error that the user can understand
The above exception is tidy, and it's reasonable to do because we know
371
+
exactly where the code is expected to fail.
372
+
373
+
The disadvantage to breaking exception chains is that you might *not*
374
+
know what is going to cause the exception, and by removing the traceback,
375
+
you hide potentially valuable information.
376
+
377
+
To add information without raising a new exception, you can use the
378
+
{meth}`Exception.add_note` method and then re-raise the same error:
379
+
380
+
```{code-cell} ipython3
381
+
---
382
+
tags: [raises-exception]
383
+
---
384
+
def read_file(file_path):
385
+
try:
386
+
with open(file_path, 'r') as file:
387
+
data = file.read()
388
+
return data
389
+
except FileNotFoundError as e:
390
+
e.add_note("Here's the deal, we both know that file should have been there, but now its not ok?")
391
+
raise e
392
+
393
+
# Raises an error immediately if the file doesn't exist
394
+
file_data = read_file("nonexistent_file.txt")
395
+
```
396
+
298
397
(pythonic-checks)=
299
398
## Make Checks Pythonic
300
399
@@ -374,3 +473,19 @@ slideshow:
374
473
---
375
474
376
475
```
476
+
477
+
---
478
+
479
+
[^more_forms]: See the [python tutorial on exceptions](https://docs.python.org/3/tutorial/errors.html#enriching-exceptions-with-notes) for the other forms that an exception might take, like:
480
+
481
+
```python
482
+
try:
483
+
# do something
484
+
except ExceptionType:
485
+
# catch an exception
486
+
else:
487
+
# do something if there wasn't an exception
488
+
finally:
489
+
# do something whether there was an exception or not
Note that the function could be written to convert the values first and then calculate the mean. However, given that the function will complete both tasks and return the mean values in the desired units, it is more efficient to calculate the mean values first and then convert just those values, rather than converting all of the values in the input array.
191
191
192
+
````{tip}
193
+
Typically functions should have a [single purpose](https://en.wikipedia.org/wiki/Separation_of_concerns), and be [composed](https://en.wikipedia.org/wiki/Function_composition_(computer_science)) to implement higher-order operations. The above function would normally be written without wrapping {func}`numpy.mean` like this:
194
+
195
+
```python
196
+
mm_to_in(np.mean(data, axis=axis_value))
197
+
```
198
+
199
+
The purpose of this lesson is to introduce function parameters, so let's focus on that for now and save design principles for another lesson :).
200
+
````
201
+
192
202
193
203
Last, include a docstring to provide the details about this function, including a brief description of the function (i.e. how it works, purpose) as well as identify the input parameters (i.e. type, description) and the returned output (i.e. type, description).
0 commit comments