Python Trace Tables

The ability to trace the values of variables during program execution is a great aid in ensuring your code is doing what it is supposed to do and in avoiding logic errors – those pesky bugs which don’t crash your program but give you unexpected results or even slip by you unnoticed only to come back and bite you later on.

In some Computer Science syllabuses this skill comes under the topic of Corrective maintenance. For example Cambridge Computer Science A Level has the following objectives relating to this topic:

  • Perform white-box testing by:
    • selecting suitable data
    • using a trace table
  • Identify any error(s) in the algorithm by using the completed trace table
  • Amend the algorithm if required

One way to keep track of values as a program executes is by using a trace table. The ability to create these is very helpful in the real-world development of software solutions as well as often being tested in Computer Science exam papers. While is is certainly worth being able to trace an algorithm manually, on paper or a whiteboard, is is also very useful to be able to do this programatically, so you can see the values of your variables at every step in a program’s execution for a given input.

There are several ways to achieve this with Python. We will look at two in this article.

Tracing Python Variables Using the sys Module

Here’s a very basic example showing how to trace the values of variables in a function call using the Python sys module.

A couple of things to note are:

  • If you want to trace code which is not in a function, you will need to “cheat” by putting it inside a function such as main() as in the example below. This is because the trace function given works by inspecting function call frames.

  • When you have called the function you wish to trace, you need to add sys.settrace(None) of you will get a whole lot of extra output which will likely not make much sense.

import sys


def trace(frame, event, arg_unused):
    print((event, frame.f_lineno, frame.f_locals))
    return trace


def main():
    x = 10
    y = 20


sys.settrace(trace)
main()
sys.settrace(None)

Output:

('call', 9, {})
('line', 10, {})
('line', 11, {'x': 10})
('return', 11, {'x': 10, 'y': 20})
>>>

So what is going on here?

Well, we have told Python to use our user-defined function trace() to produce a trace of any function calls we make. So when well call main() the trace is created and output. If you look closely at the output for the given function, you can see a line-by-line listing of the event, the lineno (line number), and the f_locals – i.e. the local variables for the function currently being executed.

Pretty cool huh?

Let’s look at a more complex example.

Tracing the Fibonacci Function in Python

import sys


def trace(frame, event, arg_unused):
    print((event, frame.f_lineno, frame.f_locals))
    return trace


sys.settrace(trace)


def fibonacci_iterative(n):
    a, b = 0, 1
    for i in range(n):
        a, b = b, a + b
    return a


fibonacci_iterative(4)
sys.settrace(None)

Output:

('call', 12, {'n': 4})
('line', 13, {'n': 4})
('line', 14, {'n': 4, 'a': 0, 'b': 1})
('line', 15, {'n': 4, 'a': 0, 'b': 1, 'i': 0})
('line', 14, {'n': 4, 'a': 1, 'b': 1, 'i': 0})
('line', 15, {'n': 4, 'a': 1, 'b': 1, 'i': 1})
('line', 14, {'n': 4, 'a': 1, 'b': 2, 'i': 1})
('line', 15, {'n': 4, 'a': 1, 'b': 2, 'i': 2})
('line', 14, {'n': 4, 'a': 2, 'b': 3, 'i': 2})
('line', 15, {'n': 4, 'a': 2, 'b': 3, 'i': 3})
('line', 14, {'n': 4, 'a': 3, 'b': 5, 'i': 3})
('line', 16, {'n': 4, 'a': 3, 'b': 5, 'i': 3})
('return', 16, {'n': 4, 'a': 3, 'b': 5, 'i': 3})
>>>

Tracing Recursive Function Calls in Python

Another situation where you might want to trace the execution of a program is to better understand what happens when a recursive function is called. You can read more about recursion here.

This can be achieved using the above method, as for example in the following example:

import sys


def trace(frame, event, arg_unused):
    print((event, frame.f_lineno, frame.f_locals))
    return trace


sys.settrace(trace)


def fibonacci_recursive(n):
    if n < 2:
        return n
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)


fibonacci_recursive(4)
sys.settrace(None)

Output:

('call', 12, {'n': 4})
('line', 13, {'n': 4})
('line', 15, {'n': 4})
('call', 12, {'n': 3})
('line', 13, {'n': 3})
('line', 15, {'n': 3})
('call', 12, {'n': 2})
('line', 13, {'n': 2})
('line', 15, {'n': 2})
('call', 12, {'n': 1})
('line', 13, {'n': 1})
('line', 14, {'n': 1})
('return', 14, {'n': 1})
('call', 12, {'n': 0})
('line', 13, {'n': 0})
('line', 14, {'n': 0})
('return', 14, {'n': 0})
('return', 15, {'n': 2})
('call', 12, {'n': 1})
('line', 13, {'n': 1})
('line', 14, {'n': 1})
('return', 14, {'n': 1})
('return', 15, {'n': 3})
('call', 12, {'n': 2})
('line', 13, {'n': 2})
('line', 15, {'n': 2})
('call', 12, {'n': 1})
('line', 13, {'n': 1})
('line', 14, {'n': 1})
('return', 14, {'n': 1})
('call', 12, {'n': 0})
('line', 13, {'n': 0})
('line', 14, {'n': 0})
('return', 14, {'n': 0})
('return', 15, {'n': 2})
('return', 15, {'n': 4})
>>>

This is fine up to a point, but there is a way to get a clearer trace for recursive algorithms.

To use it, create a file with the following code in the same folder as where the recursive code you want to trace is:

# trace_recursion.py

from functools import wraps


def trace(func):
    # Store function name, for later use
    func_name = func.__name__
    separator = '|  '  # Used in trace display

    # Set the current recursion depth
    trace.recursion_depth = 0

    @wraps(func)
    def traced_func(*args, **kwargs):
        # Display function call details
        print(f'{separator * trace.recursion_depth}|-- {func_name}({", ".join(map(str, args))})')
        # Begin recursing
        trace.recursion_depth += 1
        result = func(*args, **kwargs)
        # Exit current level
        trace.recursion_depth -= 1
        # Display return value
        print(f'{separator * (trace.recursion_depth + 1)}|-- return {result}')

        return result

    return traced_func

Then you can use the trace helper function to get a nice, easy to read representation of the recursive calls, their arguments and their return values.

For example:

from trace_recursion import trace


def factorial(n):
    if n <= 1:
        # Base case
        return 1
    else:
        # Recursive case
        return n * factorial(n - 1)


factorial = trace(factorial)
factorial(5)
``

to get the super-handy output:

|-- factorial(5)
|  |-- factorial(4)
|  |  |-- factorial(3)
|  |  |  |-- factorial(2)
|  |  |  |  |-- factorial(1)
|  |  |  |  |  |-- return 1
|  |  |  |  |-- return 2
|  |  |  |-- return 6
|  |  |-- return 24
|  |-- return 120
>>>

This article has shown you two ways you can get Python to give you valuable information about the your programs by tracing their execution. Try these methods out for yourself with your own programs if you need to develop your understanding of a particular algorithm or to debug a logic error.

Happy computing!

Sharing is caring!

2 Comments on “Python Trace Tables

  1. This article is helpful. But, this article needs more clarity. After having read this article, the questions that arise in my mind are:

    (1) The title of the article is "Python Trace Tables". But the article failed to define or explain what is a trace table "trace table" . The reader is left to presume what could be a trace table.
    (2) What is white box testing? (I read somewhere else on the web about black box. I do not know what is black box). It would have been better if the author could explain what is a white box and white box testing.
    (3) "Trace" function and "call frames": What all a "Trace" function can do or achieve? (Other than what is given in this article.)
    (4) In the out put "call", "line" and "return" are shown. Can they be numbered? Is it possible to get call No. (call number), line No. and return number?
    (5) Which is the trace helper function, in the example code given in this article?
    (6) From code example in the article: "from functools import wraps": What are wraps?

    I am stuck in an unending loop in my code for a complex large project of data frames. I am not able to solve it. Hence I needed to know what caused the unending loop! Though this article could help me, I have to try.

    I appreciate the efforts put in writing this article. I found that most of the article writers presume that certain aspects or technicalities are known to the readers who search to know details. This is where the article fails to meet it's goal. The articles must detail the fundamental aspects presuming that the readers do not know anything about the subject matter of the article.

    1. Different people’s hilltops. From your point of view you appear to assume the goal of the article was to meet you at your exact point in your learning journey where it would be most helpful. My goal was in fact not that at all. It was to share something that I thought would interesting and helpful to some people, not everybody. It sounds like what you need/want is a course that covers the fundamentals upwards, and leaves nothing uncovered. My blog does not claim to do that. One of the key skills (perhaps the most important) as a self-learner is identifying relevant resources and determining which parts are of use at your current stage and which are not. Take what is useful, and leave the rest, maybe revisiting later when you have the requisite background.

      As for your particular problem, I don’t have time to go into the details, but I suggest you try playing/experimenting with the code in the article and seeing if you can get a sense for how it works. Then try making some changes to see if you can make it work for your particular needs. Perhaps better still, try tracing your code manually on a whiteboard before even writing any code – you may find that gives you a deep insight into the issue you are facing.

Leave a Reply

Your email address will not be published. Required fields are marked *