Skip to content

✢ Corrections

Learning Objectives

At the end of this sub-unit, students should

  • know how to use information from IDLE to fix problems.
  • know how to use debug printing to find source of errors.

IDLE Information

Starting from Python 3.11, error messages for syntax error becomes much better. Consider the following simple error due to lowercase and uppercase variables. Notice the error message from IDLE.

1
2
3
>>> x = 1
>>> y = X + 1
NameError: name 'X' is not defined. Did you mean: 'x'?

In the case of other kinds of errors, the error messages may not be as clear. But there are still error messages. We can use these error messages to try to narrow down the problem. Consider the following code.

Buggy.py
def p1(x, y):
  a = p2(x, y)
  b = p3(x, y)
  return a + b

def p2(z, w):
  return z * w

def p3(a, b):
  return p2(a) + p2(b)
1
2
3
4
5
6
7
8
9
>>> p1(1, 2)
​Traceback (most recent call last):
​  File "<pyshell#2>", line 1, in <module>
​    p1(1, 2)
​  File "Buggy.py", line 3, in p1
​    b = p3(x, y)
​  File "Buggy.py", line 10, in p3
​    return p2(a) + p2(b)
​TypeError: p2() missing 1 required positional argument: 'w'

To debug this, we need to act like a detective. The information are given, we need to look at it closely and see where it is coming from. The most relevant information is at the bottom. That is the immediate cause of the problem. Often, it is sufficient to only look at this. But let us try to debug from the top.

Buggy.py
def p1(x, y):
  a = p2(x, y)
  b = p3(x, y)
  return a + b

def p2(z, w):
  return z * w

def p3(a, b):
  return p2(a) + p2(b)
1
2
3
4
5
6
7
8
9
>>> p1(1, 2)
​Traceback (most recent call last):
​  File "<pyshell#2>", line 1, in <module>
​    p1(1, 2)
​  File "Buggy.py", line 3, in p1
​    b = p3(x, y)
​  File "Buggy.py", line 10, in p3
​    return p2(a) + p2(b)
​TypeError: p2() missing 1 required positional argument: 'w'

Explanation

Here we see that we are invoking p1(1, 2) in the python shell (i.e., IDLE). We know this from "<pyshell#2>" that indicates this comes from shell. This function call will eventually end up with an error.

Buggy.py
def p1(x, y):
  a = p2(x, y)
  b = p3(x, y)
  return a + b

def p2(z, w):
  return z * w

def p3(a, b):
  return p2(a) + p2(b)
1
2
3
4
5
6
7
8
9
>>> p1(1, 2)
​Traceback (most recent call last):
​  File "<pyshell#2>", line 1, in <module>
​    p1(1, 2)
​  File "Buggy.py", line 3, in p1
​    b = p3(x, y)
​  File "Buggy.py", line 10, in p3
​    return p2(a) + p2(b)
​TypeError: p2() missing 1 required positional argument: 'w'

Explanation

Here we see inside the function p1, we are invoking p3(x, y). The line number is given by IDLE. This function call will eventually end up with an error.

Buggy.py
def p1(x, y):
  a = p2(x, y)
  b = p3(x, y)
  return a + b

def p2(z, w):
  return z * w

def p3(a, b):
  return p2(a) + p2(b)
1
2
3
4
5
6
7
8
9
>>> p1(1, 2)
​Traceback (most recent call last):
​  File "<pyshell#2>", line 1, in <module>
​    p1(1, 2)
​  File "Buggy.py", line 3, in p1
​    b = p3(x, y)
​  File "Buggy.py", line 10, in p3
​    return p2(a) + p2(b)
​TypeError: p2() missing 1 required positional argument: 'w'

Explanation

Here we see inside the function p3, we are invoking p2(a) + p2(b) at line 10. This is the source of error because this is the last lines before the actual error message.

Buggy.py
def p1(x, y):
  a = p2(x, y)
  b = p3(x, y)
  return a + b

def p2(z, w):
  return z * w

def p3(a, b):
  return p2(a) + p2(b)
1
2
3
4
5
6
7
8
9
>>> p1(1, 2)
​Traceback (most recent call last):
​  File "<pyshell#2>", line 1, in <module>
​    p1(1, 2)
​  File "Buggy.py", line 3, in p1
​    b = p3(x, y)
​  File "Buggy.py", line 10, in p3
​    return p2(a) + p2(b)
​TypeError: p2() missing 1 required positional argument: 'w'

Explanation

The very last line shows the actual cause of the error. IDLE is telling us the call p2(a) or p2(b) (or both) is missing an argument. The missing argument is w which is the second argument.

So by following the error messages slowly, we can narrow down the source of the problem. After we narrow down the source, the next step is to make corrections based on the problem requirements.

Only on Execution

Note that the error message as shown above will only be printed if there is an actual error encountered when running the program. Consider the following code.

Invert.py
1
2
3
4
5
6
7
8
9
def invert(val, op):
  if op == 'add':
    return 0 - val
  elif op == 'mul':
    return 1 / val
  elif op == 'none':
    return nothing   # no such variable!
  else:
    return 0

No Error

1
2
3
4
>>> invert(0, 'add')
0
>>> invert(1, 'mul')
1.0

Error

1
2
3
4
>>> invert(0, 'mul')
ZeroDivisionError: division by zero
>>> invert(100, 'none')
NameError

Additional Information

In some cases, information given by IDLE may not be sufficient to identify error caused by logic. In such cases, we want additional information. These information can be as simple as knowing which lines are actually executed to a more complete information of the state of the program at a particular line. The choices are up to you.

We will show one possible way to perform debugging that can be easily toggled. This is called debug printing, shortened to dprint. The idea is to have a function dprint that will print only if a particular flag is set to True.

Debug Print

1
2
3
4
DEBUG = True  # set to False to avoid printing
def dprint(tag, var, val):
  if DEBUG:
    print(tag, var, val)

How can we use this to get additional information? Consider the following incorrect factorial function.

Factorial

1
2
3
4
5
6
def factorial(n):
  res, val = 1, 0
  while val < n:
    res = res * val
    val = val + 1
  return res
>>> factorial(10)
0

If you already know the cause, kudos1 to you. But if you are having problem determining the cause, we can add debug printing and see what is happening in more details.

def factorial(n):
  res, val = 1, 0
  while val < n:
    dprint('>>', 'res', res)  # >> to indicate start of loop
    dprint('>>', 'val', val)
    res = res * val
    val = val + 1
    dprint('<<', 'res', res)  # >> to indicate end of loop
    dprint('<<', 'val', val)
  return res
>>> factorial(5)
>> res 1
>> val 0
<< res 0
<< val 1
>> res 0
>> val 1
<< res 0
<< val 2
>> res 0
>> val 2
<< res 0
<< val 3
>> res 0
>> val 3
<< res 0
<< val 4
>> res 0
>> val 4
<< res 0
<< val 5
0

That is quite long. We need to be patient and slowly trace through the debug message. Here we can see that at Line 4, we have << res 0. But we started from res = 1, how can it get reduced to zero? So we look at the operation that modifies res and we see res = res * val.

Now we know that it depends on the value of the variable val. So we look at nearby information about val. Notice that at Line 3, we have >> val 0. Ah, this is not correct because anything times 0 will give 0. So we should not use 0. The correction is that we start val from 1.

Factorial

1
2
3
4
5
6
def factorial(n):
  res, val = 1, 1  # 1 and not 0
  while val < n:
    res = res * val
    val = val + 1
  return res
1
2
3
>>> DEBUG = False  # avoid printing
>>> factorial(10)
362880

Correct by Design

The best advice we can give to avoid such error is to do designing before actually writing any code. This will be explained more clearly in the subsequent units.

Still, once the number of functions increase, we need to remember all the potential peculiarities of each function. We would recommend writing "documentation string" on the function that states the assumption clearly. Consider the following factorial function again. This function is correct assuming that the input is always non-negative.

1
2
3
4
5
6
def factorial(n):
  res, val = n, n - 1
  while val != 0:
    res = res * val
    val = val - 1
  return res

Is this a good code? If the problem description assumes that all values will be non-negative, then it solves the problem. But if we are using it with negative number, we will get an infinite loop. So there is an argument to make the function as general as possible.

Unfortunately, that may not be possible in all cases. So a compromise is to annotate our function to specify our assumptions very very clearly. We recommend using "documentation string" as follows.

def factorial(n):
  """
  Assume n is a non-negative integer (i.e., n >= 0).
  Computes and returns the value of factorial of n.
  """
  res, val = n, n - 1
  while val != 0:
    res = res * val
    val = val - 1
  return res

What is the advantage of writing it that way? We may be writing this function a week before and by now, we forgot about the assumption that it only works for non-negative input. By writing the assumption, we can refresh our memories. Additionally, this information will be displayed by IDLE!

DocString


  1. Shinichi Kudos to you because you are a great detective!