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])