Skip to content

Commit 9f2d766

Browse files
committed
Add error handling wip
1 parent f0195e5 commit 9f2d766

File tree

2 files changed

+242
-0
lines changed

2 files changed

+242
-0
lines changed

app/templates/25_error_handling.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<<[prev]({{int_linter}}) [index]({{int_index}})
2+
3+
[TOC]
4+
5+
*work in progress*
6+
7+
# Exception Handling
8+
9+
Sometimes, things go wrong.
10+
If your code attempts an unsupported operation, such as dividing by zero or accessing a key in a dictionary that doesn't exist, an exception is thrown.
11+
An unhandled exception causes the program to halt, but sometimes, we can recover from these errors and want different behavior.
12+
Let's explore how to handle exceptions!
13+
14+
## Illustrating the Problem
15+
16+
Consider the following function:
17+
```py
18+
def delta(x):
19+
return abs(1 / x)
20+
```
21+
For what values will x work?
22+
You can try calling the function with a few:
23+
```
24+
>>> delta(1)
25+
1
26+
>>> delta(2)
27+
0.5
28+
>>> delta(-2)
29+
0.5
30+
>>> delta(3.14)
31+
0.3184713375796178
32+
```
33+
These all seem to work OK.
34+
35+
What happens though, if the number is 0?
36+
```
37+
>>> delta(0)
38+
ZeroDivisionError: division by zero
39+
```
40+
41+
What happens if x is not a number?
42+
43+
```
44+
>>> delta('foo')
45+
TypeError: unsupported operand type(s) for /: 'int' and 'str'
46+
```
47+
48+
We can solve this problem in a few ways.
49+
We could have a guard statement before the dangerous division operation to protect against divide by zero:
50+
51+
```py
52+
def delta(x):
53+
if x == 0:
54+
return None
55+
return abs(1 / x)
56+
```
57+
58+
To protect against the passing of a non-number, we could use the Python builtin `isinstance` to check if x is an int or float:
59+
60+
```py
61+
def delta(x):
62+
if x == 0 or not (isinstance(x, float) or isinstance(x, int)):
63+
return None
64+
return abs(1 / x)
65+
```
66+
67+
We have introduced a new problem.
68+
We are returning two different types, None or a number, and leaving it to the user of our function to handle the possible None values.
69+
This may be OK.
70+
Additionally, the user may wish to pass something that is divisible by 1 that we haven't thought of yet: Perhaps a custom number type.
71+
Let's look at how duck typing and exceptions can allow us to handle this.
72+
73+
## Duck Typing
74+
75+
There are a variety of common ways to express duck typing.
76+
"If it looks like a duck and quacks like a duck, then it's a duck"
77+
78+
The gist of duck typing boils down to worrying about what an object can do rather than what an object inherently is.
79+
80+
Let's return to the original simple version of the function:
81+
```py
82+
def delta(x):
83+
return abs(1 / x)
84+
```
85+
86+
Instead of trying to prevent the error, let's embrace it!
87+
We can try an operation, see if it raises an exception, and then choose how to continue.
88+
To do this, we will use the try...except key words.
89+
90+
```py
91+
try:
92+
result = delta(0)
93+
except:
94+
print("That didn't work, lets carry on and try something else")
95+
```
96+
97+
Above we have a bare except: This will catch ANY error, and is awfully broad.
98+
Being indiscriminate with error catching can lead confusion or a different, harder to diagnose error later.
99+
As a general rule of thumb, we WANT to know what goes wrong in our code rather than hide it.
100+
We can narrow down the exceptions we want by specifying one or more errors:
101+
102+
```py
103+
try:
104+
result = delta(0)
105+
except ZeroDivisionError:
106+
print("You tried to divide by zero!")
107+
except TypeError:
108+
print("You tried dividing something that is not divisible!")
109+
110+
# OR
111+
112+
try:
113+
result = delta(0)
114+
except (ZeroDivisionError, TypeError):
115+
print("Whoops!")
116+
```
117+
118+
You may be curious as to what errors are possible.
119+
Hopefully, whatever function you are using has a good documentation string laying out the possibilities.
120+
Otherwise, it may come down to trial and error.
121+
Test your code thoroughly, and hopefully the exceptions will make themselves evident.
122+
123+
## Controlling the Flow
124+
125+
### else...
126+
127+
You can specify code that you wish to run after the try block ONLY if it succeeeds with the "else" keyword.
128+
129+
```py
130+
try:
131+
foo = func()
132+
except:
133+
print("Oh no!")
134+
else:
135+
process_foo(foo)
136+
```
137+
138+
### finally...
139+
140+
A common pattern is to need to clean things up regardless of if something fails or succeeds.
141+
You can accomplish this with the "finally" keyword.
142+
143+
```py
144+
import random
145+
146+
def fail_maybe():
147+
num = random.randint(0, 1)
148+
if num == 1:
149+
raise RuntimeError()
150+
151+
try:
152+
fail_maybe()
153+
except RuntimeError:
154+
print("I only print when an error occurs")
155+
else:
156+
print("I only print when no error occurs")
157+
finally:
158+
print("I will print every time")
159+
```
160+
161+
## Being Overly Broad is Bad, Part 2
162+
You may be tempted to write code like this:
163+
164+
```py
165+
try:
166+
task_1()
167+
task_2()
168+
task_3()
169+
print("About to do task 4")
170+
task_4()
171+
foo = 2 / 3 + task_5()
172+
return foo, task_6()
173+
cleanup()
174+
except:
175+
print("Something went wrong!")
176+
```
177+
178+
If "Something went wrong" gets printed...WHAT went wrong?
179+
Which of the lines were you actually worried about causing an error?
180+
You can gather error information in the except block to try and allieviate this, but that does not help the coder at read-time.
181+
In the above example, if the only task that you are concerned of raising an error is task 3, consider:
182+
183+
```py
184+
task_1()
185+
task_2()
186+
187+
try:
188+
task_3()
189+
except TaskError as e:
190+
print("Task 3 failed! {e}")
191+
else:
192+
print("About to do task 4")
193+
task_4()
194+
foo = 2 / 3 + task_5()
195+
return foo, task_6()
196+
finally:
197+
cleanup()
198+
```
199+
200+
This isn't a hard and fast rule, sometimes, it ends up being cleaner to have many statements in the try block and to sort it out with one or more except blocks.
201+
202+
## Raise Your Own Exceptions
203+
204+
You can raise exceptions in your own code as well!
205+
You may wish to do this if someone attempts to use a function you wrote in an unintended or unsupported way.
206+
You may also wish to catch several disparate errors and raise just one for the end user to worry about.
207+
208+
Use the "raise" keyword:
209+
210+
```py
211+
def func():
212+
raise RuntimeError("Optional information goes here")
213+
```
214+
215+
If you are raising an error because of another error, it is best practice to use the "from" keyword.
216+
This provides additional context to the error that shows up in the new error's stack trace.
217+
218+
```py
219+
def func(num):
220+
try:
221+
return num / 2
222+
except DivideByZeroError as e:
223+
raise RuntimeError from e
224+
```
225+
226+
## Create Your Own Exception
227+
228+
While Python provides [a large list of built-in Exceptions](https://docs.python.org/3/library/exceptions.html#concrete-exceptions), it can be useful to create your own.
229+
This allows one to narrow down to just your specific exception in exception handling code.
230+
If your exception is related to an existing one, you may inherit from it. Otherwise, inheriting from "Exception" is usually a good choice.
231+
You can create your own hierarchy of errors by creating one that inherits from "Exception", and then inheriting from that in turn.
232+
If you try to catch an exception higher up the chain, it will in turn catch its children.
233+
Remember that if you specify "Exception", you catch ALL exceptions as it is the most basic exception that all others derive from.
234+
235+
```py
236+
class MyException(Exception):
237+
"""Raised when the bad thing happens"""
238+
239+
class MyExceptionChild(MyException):
240+
pass
241+
```

app/templates/links.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@
5252
'int_debugger': f'{BASE_URL}/22_debugger.html',
5353
'int_classes': f'{BASE_URL}/23_classes.html',
5454
'int_linter': f'{BASE_URL}/24_linter.html',
55+
'int_error_handling': f'{BASE_URL}/25_error_handling.html',
5556
}
5657
# pylint: enable=line-too-long

0 commit comments

Comments
 (0)