5.2 General Rules for try-except#
If there is a suitable handler#
When an exception is raised,
if there is a try-except around the line of code that raised it,
Python checks each of the except
clauses, in order, to see if it handles the type of exception that was raised.
The first except
clause that does will handle the exception: execution will jump to its except
clause, skipping over any additional lines that may
exist in the try
block.
Then the program will continue with whatever comes after
the whole try-except statement.
For example, consider what happens in this function if num2
is 0:
def divide(num1: int, num2: int) -> None:
try:
answer = num1 / num2
print(f'The answer is {answer}')
except ZeroDivisionError:
print(f'Cannot divide {num1} by zero!')
if __name__ == '__main__':
divide(23, 0)
The assignment statement answer = num1 / num2
will raise a ZeroDivisionError
.
The except
clause matches it, so we immediately leave the try
block
and print Cannot divide 23 by zero!
.
The message saying “The answer is …” is skipped.
It is possible to end a try-except statement with a “bare” except clause, that is, one with no specific type of exception named.
try:
# Some code goes here.
pass
except ZeroDivisionError:
print('Something went wrong: attempt to divide by zero!')
except TypeError:
print('Something went wrong: type error!')
except: # No type of exception specified
print('Something went wrong: I have no idea what!')
Similarly, we can use Exception
as a wildcard that catches (almost)
every kind of exception:
try:
# Some code goes here.
pass
except ZeroDivisionError:
print('Something went wrong: attempt to divide by zero!')
except TypeError:
print('Something went wrong: type error!')
except Exception: # Very broad type of exception specified
print('Something went wrong: I have no idea what!')
In either case, PyCharm will warn us that this is a
“Too broad exception clause”.
You might wonder why, since it is similar to an if-statement that has a
final else
clause with no condition. While fine for if-statements, this is
considered bad style for exceptions.
It is good practice to be as specific as possible with the types of exceptions that we intend to handle.
If there is a kind of exception that we didn’t specifically anticipate,
or we don’t have specific code to handle,
we should allow the exception to propagate on to other code
that is better prepared to handle it.
How could there be more than one except
clause for a given exception?#
Inheritance!
A clause that says except <X>
will catch
an exception of type X or any descendant of X.
In the example below,
function nonsense
handles exceptions that are part
of a little inheritance hierarchy:
class TopException(Exception):
pass
class MiddleException(TopException):
pass
class BottomException(MiddleException):
pass
def nonsense(num: int) -> None:
try:
if num > 100:
raise TopException
elif num < 0:
raise MiddleException
elif num == 0:
raise BottomException
else:
print('All is well.')
except MiddleException:
print('A MiddleException occurred!')
If we call nonsense(-3)
, a MiddleException
will be raised
and then caught, and we will see the message A MiddleException occurred!
.
But a BottomException
is-a kind of MiddleException
, so
if we call nonsense(0)
, the BottomeException
that is raised
will be caught and handled just the same.
What if we call nonsense(142)
?
This raises a TopException
.
Since TopException
is not a kind of MiddleException
,
the exception is not caught;
instead the stack frame is popped and the exception propogates to the caller.
Suppose we want to write the code so that it can specifically handle
each of these types of exception.
Because of the way that inheritance influences the matching of a raised exception
to an except
clause, we have to be careful about the order in which
we place the except
clauses.
For example, here we put except TopException
first:
def nonsense_v2(num: int) -> None:
try:
if num > 100:
raise TopException
elif num < 0:
raise MiddleException
elif num == 0:
raise BottomException
else:
print('All is well.')
except TopException: # Catches all 3 types of exception!
print('A TopException occurred!')
except MiddleException: # Cannot be reached.
print('A MiddleException occurred!')
except BottomException: # Cannot be reached.
print('A BottomException occurred!')
But except TopException
catches all three of these types of exception, so
neither of the subsequent except
clauses can ever be reached.
If we want all three except
clauses to contribute, we must
catch the exceptions in order from most-specific to least-specific:
def nonsense_v3(num: int) -> None:
try:
if num > 100:
raise TopException
elif num < 0:
raise MiddleException
elif num == 0:
raise BottomException
else:
print('All is well.')
except BottomException:
print('A BottomException occurred!')
except MiddleException:
print('A MiddleException occurred! (and it was not a BottomException)')
except TopException:
print('A TopException occurred (and it was not a Bottom or MiddleException)')
If there is no suitable handler#
Suppose function <X>
calls function <Y>
and <Y>
raises an exception.
If there is no try-except around the line of code that raises the exception, or
there is one but it lacks an except clause for the particular kind of exception
raised,
then the stack frame for <Y>
immediately is popped.
We come back to the line
of code in <X>
that called <Y>
—and that line of code receives the
exception.
This process continues until either some function on the stack handles
the exception, or the the whole stack has been popped empty. In that case, the
user sees the exception.
Here’s an example where the call stack has several frames on it when an exception may occur:
def f3() -> None:
x = input('Enter a number: ')
print(100 / int(x))
print('That went well')
def f2() -> None:
f3()
def f1() -> None:
f2()
if __name__ == '__main__':
f1()
print('All done.')
When the code reaches f3
, the stack has on it frames for
the main block (on the bottom), f1
, f2
, and f3
(on the top).
If the user enters either 0 or something other than an integer,
an exception will be raised in f3
.
Since there is no try-except
clause, the function will immediately
return, sending the exception to f2
.
At this point, it’s no different than if f2
had raised the exception itself.
Since f2
has no try-except
clause either,
it immediately returns, sending the exception to f1
,
and f1
does the same, sending the exception to the main block.
We have just that one frame left on the stack, and it is popped too,
leaving the error message to land in the lap of the user:
Enter a number: no thanks
Traceback (most recent call last):
File "148-materials/notes/exceptions/code/popall.py", line 16, in <module>
f1()
File "148-materials/notes/exceptions/code/popall.py", line 12, in f1
f2()
File "148-materials/notes/exceptions/code/popall.py", line 8, in f2
f3()
File "148-materials/notes/exceptions/code/popall.py", line 3, in f3
print(100 / int(x))
ValueError: invalid literal for int() with base 10: 'no thanks'
Process finished with exit code 1
Below is a new version where we handle both kinds of exception.
We chose to handle any ZeroDivisionError
in f2
and any
ValueError
in f1
,
but we could have put these handlers anywhere along the chain of function calls.
def f3() -> None:
x = input('Enter a number: ')
print(100 / int(x))
print('That went well')
def f2() -> None:
try:
f3()
except ZeroDivisionError:
print('In f2 and my call to f3 raised a ZeroDivisionError')
def f1() -> None:
try:
f2()
except ValueError:
print('In f1 and my call to f2 raised a ValueError')
if __name__ == '__main__':
f1()
print('All done.')
If the user enters 0, a ZeroDivisionError
exception is raised and
the frame for f3
is popped as before, but now in f2
there is a handler that takes care of the exception and the program
can continue as if there had never been an exception raised.
Enter a number: 0
In f2 and my call to f3 raised a ZeroDivisionError
All done.
Or if the user enters something other than an integer,
a ValueError
is raised in f3
, the frame for f3
is popped,
as is the frame for f2
, since it can’t handle that type of exception.
But then we reach f1
, which has a handler for ValueError
s.
f1
catches the exception, and the program continues.
Enter a number: hee hee!
In f1 and my call to f2 raised a ValueError
All done.
If an except
clause itself raises an exception#
Here, an except
caluse itself raises an exception:
try:
# Some code goes here.
pass
except ZeroDivisionError:
n = int('ridiculous!') # Can't be handled by this try-except.
except TypeError:
print('Something went wrong: type error!')
The except
clauses in this try-except statement only handle exceptions that occur in this try
clause.
So
Python immediately stops, pops the stack, and passes the exception on to
the code that called this method or function.
That is, unless the try-except is nested inside another try-except.
In CSC148, we won’t go further into this or some of the other special cases
that can occur.
See the Python documentation if you’d like to learn more.