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!

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Join our mailing list

Join our mailing list to receive awesome articles about learning Python and Computer Science in a fun and accessible way, straight to your inbox.

 Take your Python skills to the next level!

with our free email course on object oriented programming with Python