-
Notifications
You must be signed in to change notification settings - Fork 0
Chapter 10 (Exceptions)
Through the past nine chapters, you likely have encountered cases where you code abruptly terminates because something went wrong. A logical error that causes this is termed as an exception, and generally there are a few types that are common. When an exception occurs, we call it throwing an exception. Generally, exceptions have the following format when they are thrown:
Traceback (most recent call last):
File "(filename)", line X, in <module>
(the line that caused the exception)
(exception name): (exception description)If you attempt to reproduce the example exceptions in the shell to get an understanding of how they work, do not worry if the first three lines do not match: The last line of the exception is the most important for understanding what exactly went wrong.
You may have encountered SyntaxErrors as you went through the previous chapters. If you did encounter one, you would notice that it doesn't match the format of a thrown exception described above: that would be because SyntaxErrors are not exceptions at all. Unlike exceptions, which are thrown when logic in the program does not add up, SyntaxErrors are thrown because the code itself does not make sense -- it is incomprehensible to the computer. This usually occurs due to typos, and these must be fixed before the program runs at all. Some examples of SyntaxErrors are shown below:
>>> def a()
SyntaxError: expected ':'
>>> print("Hello World"]
SyntaxError: closing parenthesis ']' does not match opening parenthesis '('
>>> def a():
print("a")
print("b")
SyntaxError: unexpected indentThere are many kinds of exceptions in Python. Here, we will be going through the types that are more commonly run into.
NameErrors occur when a term that is used, whether a variable or function, is not defined when it is trying to be used. This can arise from a few reasons. The easiest to resolve is simply that you misspelled the name of the variable, as in the example below:
>>> var1 = 5
>>> vat1
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
vat1
NameError: name 'vat1' is not defined. Did you mean: 'var1'?Python is generally kind enough to try and catch typos for you, as shown above. If this is not the case, then you should check your variable scope -- perhaps you are trying to reference a local variable from a global context, such as in the following case. If you encounter such an issue, consider returning the value that you have defined within the function scope to access it in the global scope, if it is required.
>>> def a():
b = 5
return b
>>> b
Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
b
NameError: name 'b' is not definedTypeErrors result when something is attempted but the input is of the wrong type, for example as shown below:
>>> "1" + 1
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
"1" + 1
TypeError: can only concatenate str (not "int") to strThe solution to TypeErrors is generally to use typecasting, if the operation is intentional.
>>> int("1") + 1
2ValueErrors, meanwhile, occur if the input is of the correct type, but uses an unsupported value for the operation. For example, typecasting a string that does not contain an integer to int:
>>> int("a")
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
int("a")
ValueError: invalid literal for int() with base 10: 'a'
>>> int("2.0")
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
int("2.0")
ValueError: invalid literal for int() with base 10: '2.0'Another example would be trying to take the square root of a negative number.
>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
math.sqrt(-1)
ValueError: math domain errorIndexErrors occur when an invalid index is used. This occurs when dealing with strings or lists, such as below:
>>> str1 = "abc"
>>> str1[3]
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
str1[3]
IndexError: string index out of range
>>> list1 = [1, 2, 3]
>>> list1[3]
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
list1[3]
IndexError: list index out of rangeThis commonly occurs in an "off-by-one error", where loops iterate one more time than they should, causing the attempted access of an entry one past the last location. It can also be a result of one-indexing the positions instead of zero-indexing as the language does. An example of an off-by-one error is in the following snippet:
>>> list2 = [1, 2, 3, 4, 5, 6]
>>> list3 = [1, 2, 3, 4, 5, 6]
>>> for i in range(7):
print (list2[i] + list3[i])
2
4
6
8
10
12
Traceback (most recent call last):
File "<pyshell#19>", line 2, in <module>
print (list2[i] + list3[i])
IndexError: list index out of rangeRemember that the range() function is inclusive of the lower bound and exclusive of the upper bound! For safety, if you want to iterate as many times as the list has entries, the usage of range(len(list)) is recommended.
RuntimeErrors are generic errors -- they are caused when the exception does not fall into any of the defined categories. A string will be displayed that describes the error.
ZeroDivisionErrors, as the name suggests, occur when there is an attempted division by zero.
>>> 1 / 0
Traceback (most recent call last):
File "<pyshell#20>", line 1, in <module>
1 / 0
ZeroDivisionError: division by zeroPython, like most programming languages, has an in-built way of catching thrown exceptions and handling them within the code. This allows for the program to gracefully handle the error, instead of unceremoniously terminating.
DISCLAIMER: Do NOT use this as a way to bypass exceptions! Most exceptions that are thrown can be avoided simply by modifying the code to not throw them. Using try-except to bypass such exceptions is bad style that will slow down execution. Instead, try-except should only be used to catch errors that cannot be predicted, such as errors from user input.
The syntax of exception handling contains up to four blocks. In the following section, we will slowly add the blocks, starting from the most fundamental components and slowly adding the remaining two.
The most basic form of exception handling uses try-except. The concept is simple: Whatever is in the try-block is tried out, having it run normally. If an Exception occurs, it checks all the except blocks for a the corresponding exception, and if one is found, that block runs. If none of the except blocks correspond, the exception is thrown out and the program terminates.
An example can be seen below:
>>> try:
print(2 / 0)
except ZeroDivisionError:
print("Cannot divide by zero!")
Cannot divide by zero!Note that try-catch should not be used in a similar manner to the above in real programs -- it is only to give you an idea of how the blocks interact. Using try-catch to bypass an overt division by zero as in the above should not be done. Instead, removing the offending line or changing it to avoid the exception should be the first thing you do.
Try-catch is commonly applied in user input, as stated above. A simple case would be the following, which attempts to compute the decimal value of a fraction with numerator and denominator given by the user.
i1 = input("Enter numerator")
i2 = input("Enter denominator")
try:
print(int(i1) / int(i2))
except ValueError:
print("Only integer values permitted")
except ZeroDivisionError:
print("Cannot divide by zero!")You may have noticed that a try block can be followed by more than one except block! Much like in conditionals, where elif clauses can be used as many times as required to catch as many cases as needed, you can have as many except clauses as you wish, one for each way you want to handle different exceptions.
The else-block goes after the except block. Recall from conditionals that the else-block is always placed at the end in a chain of if-elif-else, and runs if none of the preceding blocks do. As except blocks are like elif blocks to handle individual cases, the else block serves a similar function -- it runs if no exception is caught by the except blocks.
>>> try:
print(12 / 3)
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print("All good!")
4.0
All good!
>>> try:
print(12 / 0)
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print("All good!")
Cannot divide by zero!In the first case, 12 / 3 is a perfectly valid operation, yielding the result 4, which is printed. Since no except block was triggered, the else-block executes, printing the message "All good!". In the second case, the attempted division throws a ZeroDivisionError, which thus triggers the except block immediately, and causes the else-block to be skipped.
Ending off the chain would be the finally-block. While the except-block is run if the corresponding exception is thrown in the try-block, and the else-block is run if no exceptions are thrown, the finally-block doesn't care: It always runs, regardless of what happens previously. Thus, the finally-block is generally used to clean-up any operations that should be done, such as closing of file streams after an attempted read.
>>> try:
print(12 / 3)
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print("All good!")
finally:
print("Always runs")
4.0
All good!
Always runs
>>> try:
print(12 / 0)
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print("All good!")
finally:
print("Always runs")
Cannot divide by zero!
Always runsTake note that the four blocks must be positioned in this order: While you can have as many except-blocks as you want, they must precede the else-block and finally-block, and there can only be one of each of the latter two.
That's all there is to the general flow of exception handling!
By a team of students from NUS High School, aiming to make coding easier for everyone.