Python for Programmers: Handling Exceptions
Welcome to the Handling Exceptions lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Most exceptions indicate some kind of problem, so their names end in "error". For example, dividing by zero raises
ZeroDivisionError.>
1 / 0Result:
When an exception is raised, the containing function stops executing immediately. Then Python "unwinds" the call stack: it terminates the function that called this function, then terminates the function that called that function, etc. By default, this continues until we get to the topmost function. Then the entire program crashes with the exception.
We can catch an exception with a
try: ... except: ...block, which stops the unwinding process. (Other languages call thistry { ... } catch { ... }).In the next example, the line
1 / 0raises aZeroDivisionErrorinside thetryblock. Python jumps to theexcept:block so we can handle the exception.>
try:1 / 0what_ran = "try"except:what_ran = "except"what_ranResult:
'except'
We can use
except:to recover from exceptions, for example by showing the user an error message. Other times, we useexcept:to catch an exception, then raise a different kind of exception. Often, the new exception adds additional details to help with debugging.If we divide valid values, we don't raise an exception, so the
except:block doesn't run at all.>
try:1 / 2what_ran = "try"except:what_ran = "except"what_ranResult:
'try'
The
except:above catches any kind of exception, but we usually anticipate a specific exception. For example, when indexing into a dictionary, we might anticipate aKeyError. We don't want to catch other, unexpected exceptions, because they probably indicate bugs.>
config = {"retry_count": 3}try:hostname = config["hostname"]except KeyError:hostname = "default.example.com"hostnameResult:
'default.example.com'
>
config = {"retry_count": 3}try:retries = config["retry_count"]except KeyError:retries = 0retriesResult:
3
In the next example, we raise a
ZeroDivisionErrorexception again. This time, we have anexcept KeyError:. But the exception is aZeroDivisionError, not aKeyError, so ourexcept:doesn't catch it. The exception escapes from the function, reaches the top level of the call stack, and the overall code example errors. It's the same result that we'd get if we hadn't wrapped the code in atry:at all.>
try:1 / 0except KeyError:result = "caught key error"resultResult:
ZeroDivisionError: division by zero
Exceptions form a hierarchy, which lets us handle errors at different levels of granularity. For example,
ZeroDivisionErroris a kind ofArithmeticError, which is a kind ofException.Here's a section of the exception hierarchy, showing how different exceptions relate. The full hierarchy is quite large, so this is just a small sample!
- Exception
- ArithmeticError
- OverflowError (a number was too big)
- ZeroDivisionError (we divided by 0)
- LookupError
- IndexError (a list index didn't exist)
- KeyError (a dict key didn't exist)
- ArithmeticError
- Exception
We can use this hierarchy to control which exceptions we catch. For example, if some code raises a
ZeroDivisionError, then we can catch it as a generalException, or a more specificArithmeticError, or the exactZeroDivisionError. Each choice means something different. If we catchArithmeticError, then we'll also catchOverflowErrorin addition toZeroDivisionError.>
try:1 / 0except ZeroDivisionError:caught = TruecaughtResult:
True
>
try:1 / 0except ArithmeticError:caught = TruecaughtResult:
True
>
try:1 / 0except Exception:caught = TruecaughtResult:
True
We can have multiple
except:blocks, each catching a different exception type. Python runs the firstexcept:block that matches the exception.>
config = {"hostname": "api.example.com"}try:hostname = config["HOSTNAME"]except ZeroDivisionError:caught_error = "divide_by_zero"except KeyError:caught_error = "key"caught_errorResult:
'key'
When more than one
except:matches the exception, the first one "wins", and the others don't run at all. In the next example, all threeexcept:s match the exception, but only the first one runs.>
catches = []try:1 / 0except ZeroDivisionError:catches.append("zero division error")except ArithmeticError:catches.append("arithmetic error")except Exception:catches.append("exception")catchesResult:
['zero division error']
We can add a
finally:block, which always runs, whether an exception is raised or not. Its most common job is to clean up resources that we used during thetry:block. For example, we can use it to close files or network connections.In the next example, we build a list while handling exceptions. At the end of the example, we have list elements showing exactly which blocks ran.
>
trace = []try:1 / 0trace.append("try finished")except ZeroDivisionError:trace.append("caught zero division")except KeyError:trace.append("caught key error")finally:trace.append("finally")traceResult:
['caught zero division', 'finally']
We can add an
else:block, which only runs if no exception is raised. It's similar to how theelse:on anif:only runs when the condition wasn't true. This feature doesn't exist in most other programming languages, but it's very handy in some situations.We can think of
else:like anexcept:that only runs when no exception is raised. Likeexcept:, theelse:runs before thefinally:.The full process for exception handling is:
- Run the
try:. - If an exception was raised, run the first matching
except:. If no exception was raised, run theelse:. - Run the
finally:.
- Run the
First, we'll run an example that's similar to the one above, but with an
else:. An exception is raised and caught here, so theelse:doesn't run.>
trace = []try:1 / 0trace.append("try finished")except ZeroDivisionError:trace.append("caught zero division")except KeyError:trace.append("caught key error")else:trace.append("else")finally:trace.append("finally")traceResult:
['caught zero division', 'finally']
Second, we'll run code that doesn't raise an exception. The
try:block finishes successfully. Because of that success, theelse:block runs. Then thefinally:block runs, as always.>
trace = []try:1000 / 2000trace.append("try finished")except ZeroDivisionError:trace.append("caught zero division")except KeyError:trace.append("caught key error")else:trace.append("else")finally:trace.append("finally")traceResult:
['try finished', 'else', 'finally']
Here's a code problem:
The
assert_password_correctfunction below is already complete. It raisesValueErrorwhen the passwords don't match. It also raisesTypeErrorwhen the password isn't a string.Now we want an additional function,
password_error. It callsassert_password_correct, then tells us what, if anything, was wrong with the password. Build outpassword_errorto catch both of the exceptions mentioned above, and return string descriptions of the problems.- If it catches a
TypeError, return"not a string". - If it catches a
ValueError, return"wrong password". - Otherwise (
else:) returnNone.
def assert_password_correct(password):if not isinstance(password, str):raise TypeError("Passwords must be strings")if password != "hunter2":raise ValueError("Password didn't match")def password_error(password):try:assert_password_correct(password)except TypeError:return "not a string"except ValueError:return "wrong password"else:return Noneassert password_error(12) == "not a string"assert password_error("hunter3") == "wrong password"assert password_error("hunter2") is None- Goal:
None
- Yours:
None
- If it catches a