Errors and exceptions#
Author: Jeff Jennings (CCA, jjennings@simonsfoundation.org)
‘Syntax errors’ are mistakes in the grammar of Python that are checked for before code is executed.
‘Exceptions’ are ‘runtime errors’ - conditions usually caused by attempting an invalid operation. These can be caught and handled.
[123]:
import numpy as np
Syntax errors#
For example, a missing parenthesis is a syntax error:
[124]:
for x in range(10:
print(x)
Cell In[124], line 1
for x in range(10:
^
SyntaxError: invalid syntax
(Note that the caret ^
is showing us where in the line the syntax error was encountered.)
If a coherent block of code spans multiple text lines, an arrow indicates the erroneous part:
[ ]:
for x in range(10):
if x < 5:
print(x + y)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[12], line 3
1 for x in range(10):
2 if x < 5:
----> 3 print(x + y)
NameError: name 'y' is not defined
An IndentationError
is a common syntax error:
[ ]:
for x in range(10):
print(x)
Cell In[14], line 2
print(x)
^
IndentationError: expected an indented block after 'for' statement on line 1
As is use of =
rather than ==
to check for equality:
[ ]:
a = 5
if a = 5:
print("all good")
Cell In[15], line 2
if a = 5:
^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
Misuse of a reserved keyword is a bit more subtle syntax error:
[ ]:
for lambda in range(10):
print(x)
Cell In[2], line 2
for lambda in range(10):
^
SyntaxError: invalid syntax
(lambda
is a reserved keyword, so can’t be used as a variable name. A variable name was expected after for
, thus the error.)
Exceptions#
An exception is raised when a gramatically correct expression is executed and causes a runtime error. There are built-in exceptions, and you can also define your own.
If an exception occurs within a function (which may have itself been called by another function or from within a class, e.g.), the error message is a ‘stack traceback’ - a log of the history of function calls leading to the error.
For example:
[ ]:
def root_func(x, y):
return x + y + xy
def func(x, y):
z = root_func(x, y)
return x + y + z
func(1, 2)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[45], line 8
5 z = root_func(x, y)
6 return x + y + z
----> 8 func(1, 2)
Cell In[45], line 5, in func(x, y)
4 def func(x, y):
----> 5 z = root_func(x, y)
6 return x + y + z
Cell In[45], line 2, in root_func(x, y)
1 def root_func(x, y):
----> 2 return x + y + xy
NameError: name 'xy' is not defined
(Notice how the traceback sequentially moves through the chain of function calls to show the root of the error, i.e., the ‘most recent call’.)
Common exceptions#
IndexError
- Indexing a sequence (a list, array, string, etc.) with a subscript that’s out of range:
[ ]:
a = range(5)
a[5]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[47], line 2
1 a = range(5)
----> 2 a[5]
IndexError: range object index out of range
KeyError
- Indexing a dictionary with a key that doesn’t exist:
[ ]:
my_dictionary = {'a': 1, 'b': 2}
my_dictionary[1]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[51], line 2
1 my_dictionary = {'a': 1, 'b': 2}
----> 2 my_dictionary[1]
KeyError: 1
NameError
- Referencing a local or global variable name that hasn’t been defined:
[ ]:
class MyClass:
def add(self, x, y):
return x + y
add(1, 2)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[48], line 5
2 def add(self, x, y):
3 return x + y
----> 5 add(1, 2)
NameError: name 'add' is not defined
TypeError
- Attempting to pass an object of the wrong type as an argument to an operation or function:
[ ]:
1 + list([2])
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[55], line 1
----> 1 1 + list([2])
TypeError: unsupported operand type(s) for +: 'int' and 'list'
ValueError
- Attempting to pass an object of the right type but with an incompatible value as an argument to an operation or function:
[ ]:
print(float('5'))
float('abc')
5.0
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[64], line 3
1 print(float('5'))
----> 3 float('abc')
ValueError: could not convert string to float: 'abc'
(The ‘float’ function can take a string to convert to a float, so it can conver the string ‘5’. But it doesn’t know how to convert non-numerical values to a float.)
Handling exceptions#
To catch an exception, write code within a try:
clause and handle any exceptions raised in an except:
clause:
[ ]:
def inv(x):
inv_list = []
for i,j in enumerate(x):
try:
inv_list.append(1 / j)
except ZeroDivisionError as err:
print(f"{err}: x[{i}] is 0 - skipping")
print(inv_list)
inv(range(10))
division by zero: x[0] is 0 - skipping
[1.0, 0.5, 0.3333333333333333, 0.25, 0.2, 0.16666666666666666, 0.14285714285714285, 0.125, 0.1111111111111111]
Note that if any exception besides a ZeroDivisionError
is raised, it won’t be caught. We can handle multiple exceptions in a single except
statement:
[ ]:
def inv(x):
inv_list = []
for i,j in enumerate(x):
try:
inv_list.append(1 / j)
except (ZeroDivisionError, TypeError) as err:
print(f"{err}: x[{i}]")
print(inv_list)
x = [1, 2, 0, 'a', 3]
inv(x)
division by zero: x[2]
unsupported operand type(s) for /: 'int' and 'str': x[3]
[1.0, 0.5, 0.3333333333333333]
…or we can have multiple except
statements:
[ ]:
def inv(x):
inv_list = []
for i,j in enumerate(x):
try:
inv_list.append(1 / j)
except ZeroDivisionError as err:
print(f"{err}: x[{i}]")
except TypeError as err:
print(f"{err}: x[{i}] has the value {j}")
print(inv_list)
x = [1, 2, 0, 'a', 3]
inv(x)
division by zero: x[2]
unsupported operand type(s) for /: 'int' and 'str': x[3] has the value a
[1.0, 0.5, 0.3333333333333333]
Don’t handle exceptions like the following:
[ ]:
try:
1 / 0
except:
pass
This executes everything in the try block and ignores any exceptions raised. It can make code hard to debug, as errors are silently supressed. Instead of this, always catch specific exceptions and handle them appropriately.
Raising exceptions via a custom condition#
Use raise
to evoke an exception when a condition you choose occurs:
[ ]:
fluxes = np.random.rand(1000)
fluxes[50] -= 1
for i,f in enumerate(fluxes):
if f < 0:
raise ValueError(f"Negative flux value {f} at index {i}")
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[117], line 6
4 for i,f in enumerate(fluxes):
5 if f < 0:
----> 6 raise ValueError(f"Negative flux value {f} at index {i}")
ValueError: Negative flux value -0.76432315747285 at index 50
You can also use assert
to evaluate a conditional expression and raise an AssertionError
if the is not True
:
[ ]:
for i,f in enumerate(fluxes):
assert f >= 0, f"Negative flux value {f} at index {i}"
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[121], line 2
1 for i,f in enumerate(fluxes):
----> 2 assert f >= 0, f"Negative flux value {f} at index {i}"
AssertionError: Negative flux value -0.76432315747285 at index 50
assert
is often used to check the type or some other aspect of an input to a function:
[ ]:
def cross_product(a, b):
assert len(a) == len(b) == 3, "Vectors a, b must be three-dimensional"
return [a[1]*b[2] - a[2]*b[1], \
a[2]*b[0] - a[0]*b[2], \
a[0]*b[1] - a[1]*b[0]
]
print(cross_product([1,2,3], [4,5,6]))
cross_product([1,2,3], [4,5])
[-3, 6, -3]
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[122], line 10
3 return [a[1]*b[2] - a[2]*b[1], \
4 a[2]*b[0] - a[0]*b[2], \
5 a[0]*b[1] - a[1]*b[0]
6 ]
8 print(cross_product([1,2,3], [4,5,6]))
---> 10 cross_product([1,2,3], [4,5])
Cell In[122], line 2, in cross_product(a, b)
1 def cross_product(a, b):
----> 2 assert len(a) == len(b) == 3, "Vectors a, b must be three-dimensional"
3 return [a[1]*b[2] - a[2]*b[1], \
4 a[2]*b[0] - a[0]*b[2], \
5 a[0]*b[1] - a[1]*b[0]
6 ]
AssertionError: Vectors a, b must be three-dimensional
When writing a custom exception, it’s always good to check that the condition behaves as you expect with a quick sanity check. For example:
[128]:
a = np.nan
print(a)
a == np.nan
nan
[128]:
False
(np.nan
is not equal to anything, not even itself! So if you had an input dataset and wanted to skip NaN values, this check would remind you not to raise an exception using if x == np.nan: ...
. Instead in this case use np.isnan
:)
[132]:
np.isnan([1,2,a])
[132]:
array([False, False, True])