Skip to content

Commit 95da92d

Browse files
sneakers-the-ratlwasser
authored andcommitted
refinements on exception handling
1 parent 639ccfb commit 95da92d

File tree

2 files changed

+155
-45
lines changed

2 files changed

+155
-45
lines changed

code-workflow-logic/python-function-checks.md

Lines changed: 154 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -99,31 +99,12 @@ print(clean_title("hi, i am a title"))
9999
print(clean_title(""))
100100
```
101101

102-
+++ {"editable": true, "slideshow": {"slide_type": ""}}
103-
104-
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!
105105
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.
127108
```
128109

129110
+++ {"editable": true, "slideshow": {"slide_type": ""}}
@@ -138,7 +119,7 @@ Here’s how try/except blocks work:
138119
* **try block:** You write the code that might cause an error here. Python will attempt to run this code.
139120
* **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.
140121

141-
A `try/except` block looks like this:
122+
A `try/except` block looks like this[^more_forms]:
142123

143124
```python
144125
try:
@@ -213,7 +194,7 @@ file_data = read_file("nonexistent_file.txt")
213194

214195
+++ {"editable": true, "slideshow": {"slide_type": ""}}
215196

216-
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.
217198

218199
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.
219200

@@ -227,7 +208,7 @@ slideshow:
227208
---
228209
import os
229210
230-
def read_file(file_path):
211+
def read_file_silent(file_path):
231212
if os.path.exists(file_path):
232213
with open(file_path, 'r') as file:
233214
data = file.read()
@@ -236,24 +217,107 @@ def read_file(file_path):
236217
return None # Doesn't fail immediately, just returns None
237218
238219
# No error raised, even though the file doesn't exist
239-
file_data = read_file("nonexistent_file.txt")
220+
file_data = read_file_silent("nonexistent_file.txt")
240221
```
241222

242223
+++ {"editable": true, "slideshow": {"slide_type": ""}}
243224

244-
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.
245226

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:
228+
229+
```python
230+
# The problem occurs here...
231+
data = read_file_silent("nonexistent_file.txt")
232+
233+
# This file takes an hour to download...
234+
big_file = download_file('http://example.com/big_file.exe')
249235

250-
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!
251318

252319
```{code-cell} ipython3
253320
---
254-
editable: true
255-
slideshow:
256-
slide_type: ''
257321
tags: [raises-exception]
258322
---
259323
def read_file(file_path):
@@ -264,18 +328,12 @@ def read_file(file_path):
264328
except FileNotFoundError:
265329
raise FileNotFoundError(f"Oops! I couldn't find the file located at: {file_path}. Please check to see if it exists.")
266330
267-
# Raises an error immediately if the file doesn't exist
331+
268332
file_data = read_file("nonexistent_file.txt")
269333
```
270334

271-
## Customizing error messages
335+
Instead we can use the exception chaining syntax, `raise {exception} from {other exception}`, to explicitly exclude the original error from the traceback.
272336

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

280338
```{code-cell} ipython3
281339
---
@@ -289,12 +347,47 @@ def read_file(file_path):
289347
except FileNotFoundError:
290348
raise FileNotFoundError(f"Oops! I couldn't find the file located at: {file_path}. Please check to see if it exists.") from None
291349
292-
# Raises an error immediately if the file doesn't exist
350+
293351
file_data = read_file("nonexistent_file.txt")
294352
```
295353

354+
This code example below is better than the examples above for three reasons:
355+
356+
1. It's **pythonic**: it asks for forgiveness later by using a try/except
357+
2. It fails quickly - as soon as it tries to open the file. The code won't continue to run after this step fails.
358+
3. It raises a clean, useful error that the user can understand
359+
296360
+++ {"editable": true, "slideshow": {"slide_type": ""}}
297361

362+
### Adding Information - Using Notes
363+
364+
The above exception is tidy, and it's reasonable to do because we know
365+
exactly where the code is expected to fail.
366+
367+
The disadvantage to breaking exception chains is that you might *not*
368+
know what is going to cause the exception, and by removing the traceback,
369+
you hide potentially valuable information.
370+
371+
To add information without raising a new exception, you can use the
372+
{meth}`Exception.add_note` method and then re-raise the same error:
373+
374+
```{code-cell} ipython3
375+
---
376+
tags: [raises-exception]
377+
---
378+
def read_file(file_path):
379+
try:
380+
with open(file_path, 'r') as file:
381+
data = file.read()
382+
return data
383+
except FileNotFoundError as e:
384+
e.add_note("Here's the deal, we both know that file should have been there, but now its not ok?")
385+
raise e
386+
387+
# Raises an error immediately if the file doesn't exist
388+
file_data = read_file("nonexistent_file.txt")
389+
```
390+
298391
(pythonic-checks)=
299392
## Make Checks Pythonic
300393

@@ -374,3 +467,19 @@ slideshow:
374467
---
375468
376469
```
470+
471+
---
472+
473+
[^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:
474+
475+
```python
476+
try:
477+
# do something
478+
except ExceptionType:
479+
# catch an exception
480+
else:
481+
# do something if there wasn't an exception
482+
finally:
483+
# do something whether there was an exception or not
484+
```
485+

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies = [
2424
# for project cards
2525
"matplotlib",
2626
"pandas",
27+
"rich",
2728
]
2829

2930
[project.optional-dependencies]

0 commit comments

Comments
 (0)