If you work with Python, you are probably familiar with the traceback concept. Traceback represents the Python call stack state when a program crashes. Whenever the code gets an exception, the traceback will provide the information on what went wrong.
As a quick reminder, when Python calls a function, an object called stack frame is pushed into the call stack. The call stack keeps track of function calls. Each frame in the stack keeps track of function arguments, local variables, and so on. When the function returns the expected output, the stack frame disappears from the top of the call stack.
A stack frame represents a single function call. You can visualize functions that call one another as virtual frames stacking on top of each other. For this purpose, Python uses a special stack data structure.
In this topic, we will take a closer look at the built-in traceback module, but before we do that, let's see how the Python interpreter generates the traceback error.
Traceback insights
import random
output = random.randint(-5, -10)
Traceback (most recent call last):
File "C:\Users\dandei\Desktop\problems.py", line 9, in <module>
out = random.randint(-5,-10)
File "C:\Python39\lib\random.py", line 338, in randint
return self.randrange(a, b+1)
File "C:\Python39\lib\random.py", line 316, in randrange
raise ValueError("empty range for randrange() (%d, %d, %d)" % (istart, istop, width))
ValueError: empty range for randrange() (-5, -9, -4)
In the example above, we can see that the error traceback generated by the Python interpreter is not always that clear and informative. We may need to modify the stack traces according to our needs when we need more control over the printed traceback. We may also need more control over the traceback format. Here is where the built-in traceback module comes in handy. It contains methods that let us extract error traces, format, and print them. This can help with formatting and limiting the error trace.
According to the official documentation, the traceback module provides the standard interface with the methods that fall into several common categories and are available for extracting, formatting, and printing stack traces. It is similar to the Python interpreter when it prints a stack trace.
Traceback print methods
Parameters like limit, file, or chain are present in multiple methods of the traceback module, and they have the same meaning across all methods.
Let's start with the print methods. As their names suggest, they are used to print stack traces to the desired output target (standard error, standard output, file, and so forth).
The first method is print_tb(tb, limit=None, file=None). It accepts a traceback instance (for example, a raised error traceback) and prints traces to the desired output. It also allows us to limit the trace amount by specifying the limit parameter. If we don't specify a limit, it will print the whole stack trace. Another important parameter is file; it specifies where the trace output will be redirected to. We can give a file-like object, and it'll direct traces to it. We can also specify the standard error and standard output with sys.err and sys.out as file parameters; traces will be directed to them. If no file is specified, then by default, it'll output traces to the standard error. To get specific information from the traceback, we will use the exc_info method of the sys module that returns a tuple (exception type, exception value, exception traceback) with information about a recent exception that was caught by the try-except block. You can read more about the exc_info on the official documentation page.
import traceback
import random
import sys
try:
output = random.randint(-5,-10)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print("==================Traceback using print_tb =========================")
# we use the print_tb function with limit of 1 to limit the printed traceback
traceback.print_tb(exc_traceback, limit=1, file=sys.stdout)
==================Traceback using print_tb =========================
File "C:\Users\andre\Desktop\test.py", line 6, in <module>
output = random.randint(-5,-10)
Another useful method is print_exception(exception_type, value, tb, limit=None, file=None, chain=True). It can be used to print exception information, as well as the error stack trace. We need to provide the exception type, value, and traceback. The chain parameter is a boolean flag indicating whether we should include the trace for chained exceptions (occur when another exception gets raised inside an except block) or not.
import traceback
import random
import sys
try:
output = random.randint(-5,-10)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print("\n==================Traceback using print_exception ===================")
# here, the print_exception method will also print a header (Traceback (most recent call):),
# the exception type, and value (ValueError: empty range for randrange() (-5, -9, -4))
traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stdout)
==================Traceback using print_exception ===================
Traceback (most recent call last):
File "C:\Users\andre\Desktop\test.py", line 6, in <module>
output = random.randint(-5,-10)
File "C:\Python310\lib\random.py", line 370, in randint
return self.randrange(a, b+1)
File "C:\Python310\lib\random.py", line 353, in randrange
raise ValueError("empty range for randrange() (%d, %d, %d)" % (istart, istop, width))
ValueError: empty range for randrange() (-5, -9, -4)
Lastly, we want to mention print_exc(limit=None, file=None, chain=True) that is a shorthand for print_exception(*sys.exc_info(), limit, file, chain) and returns the same output. Whenever it is called, this method will print the last exception that happened in the program.
Traceback extract methods
There are two methods for extracting in the traceback module. The first one is extract_tb(tb, limit=None) that accepts a traceback object as a parameter and returns an object representing a list of the "pre-processed" stack trace entries (an object containing attributes: filename, lineno, name, and line) extracted from the traceback object tb. It is useful for changing the formatting of stack traces:
import traceback
import random
import sys
try:
out = random.randint(-5,-10)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
# the extract_tb method returns an object with multiple attributes
# that let us print out the traces in a nicely formatted way
traces = traceback.extract_tb(exc_traceback)
print("%40s | %10s | %5s | %10s" %("File Name", "Method Name", "Line Number", "Line"))
print("-"*100)
for frame_summary in traces:
print("%40s | %11s | %11d | %10s"%(frame_summary.filename, frame_summary.name, frame_summary.lineno, frame_summary.line))
print("-"*100)
File Name | Method Name | Line Number | Line
----------------------------------------------------------------------------------------------------
C:\Users\dandei\Desktop\problems.py | module | 7 | out = random.randint(-5,-10)
----------------------------------------------------------------------------------------------------
C:\Python39\lib\random.py | randint | 338 | return self.randrange(a, b+1)
----------------------------------------------------------------------------------------------------
C:\Python39\lib\random.py | randrange | 316 | raise ValueError("empty range for randrange() (%d, %d, %d)" % (istart, istop, width))
----------------------------------------------------------------------------------------------------------------------------------------------
The second one is extract_stack(f=None, limit=None) which extracts raw traceback from the current stack frame. The return value has the same format as the extract_tb method. Let's see it in action:
import traceback
import random
try:
output = random.randint(-5,-10)
except Exception as e:
tbk = e.__traceback__
# we use the extract_stack method to create a list of
# trace frames and print them in a readable way
while tbk:
trace = list(traceback.extract_stack(tbk.tb_frame, limit=1))[0]
print("========== Trace ==========")
print("Line No : ", trace.lineno)
print("%35s | %11s | %10s"%(trace.filename, trace.name, trace.line))
print()
tbk = tbk.tb_next
========== Trace ==========
Line No : 13
C:\Users\andre\Desktop\test.py | <module> | trace = list(traceback.extract_stack(tbk.tb_frame, limit=1))[0]
========== Trace ==========
Line No : 370
C:\Python310\lib\random.py | randint | return self.randrange(a, b+1)
========== Trace ==========
Line No : 353
C:\Python310\lib\random.py | randrange | raise ValueError("empty range for randrange() (%d, %d, %d)" % (istart, istop, width))Traceback format methods
We can also format the stack trace after its extraction. We can do it with the format functions included in the traceback module. These functions can be useful if we want to print an error stack because it returns a list of strings with formatted messages.
format_list(extracted_list) takes a list of tuples as an argument (for example the result of extract_tb() or extract_stack()) and returns a list of strings ready for printing; each string ending with a newline. Every string in the resulting list corresponds to the item with the same index in the argument list.
import traceback
import random
import sys
try:
output = random.randint(-5,-10)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
# we use the extract_tb method to extract the traceback
traces = traceback.extract_tb(exc_traceback)
print(" \n========== Trace Format using format_list ===============\n")
# the format_list method returns a list of formated strings each ending with "\n"
for trace_line in traceback.format_list(traces):
print(trace_line)
========== Trace Format using format_list ===============
File "C:\Users\andre\PycharmProjects\pythonProject\main.py", line 7, in <module>
output = random.randint(-5,-10)
File "C:\Python310\lib\random.py", line 370, in randint
return self.randrange(a, b+1)
File "C:\Python310\lib\random.py", line 353, in randrange
raise ValueError("empty range for randrange() (%d, %d, %d)" % (istart, istop, width))
Another useful function is format_exception() that works in the same way as print_exception(). The major difference is that it returns a list of strings where each string is a single trace of the stack.
The last thing is format_exception_only(exception_type, exception_value). It takes the exception type and value as input and returns a list of strings specifying the exception description. Below is a simple example:
import traceback
import random
try:
output = random.randint(-5,-10)
except Exception as e:
print(" \n========== Trace Format using format_exception_only ===============\n")
for exception_line in traceback.format_exception_only(type(e), e):
print(exception_line)
========== Trace Format using format_exception_only ===============
ValueError: empty range for randrange() (-5, -9, -4)
There are also shorthand functions for formatting the traceback error, for example, format_tb(). It works the same as print_tb() with the only difference that it returns a list of strings, where each string is a single trace of the stack. We won't get into details about these functions; they are worthy of a more detailed discussion later, but you can always read more in the official documentation.
Conclusion
Learning how to read the error traceback is extremely important for us as developers. It is one of the first steps in learning to "speak" the computer language. Now, we've taken a step further. We've seen how we can extract information from the traceback, how to format it conveniently, and how to print it. We can do it with the help of the traceback module functions like extract_tb(), format_list() or print_tb(). There are many other aspects to cover, but for now, let us practice what we've learned so far!