Analyze Python Stack Traces

David Y.
jump to solution

The Problem

I’m new to Python development and want to improve my understanding of stack traces and tracebacks. Often, when a script I’m working on runs into an error, it will crash and print out a long list of line numbers and function names, followed by an error message. Some of these functions are ones I’ve written, but most aren’t. Sometimes, Python will even print out more than one stack trace, accompanied by a line like the following:

During handling of the above exception, another exception occurred:

I understand these stack traces are intended to help with debugging, but I don’t know how to read them or what information they’re supposed to provide. Please help me make sense of stack traces.

The Solution

A stack trace shows the sequence of function calls leading up to a particular line of code, usually where an exception was thrown. If we think of debugging as the investigation of an error, the stack trace is what allows us to retrace our steps and see exactly how the error arose.

To create a simple example stack trace, we’ll use the following script, which contains a deliberate error:

def divide(a, b):
    return a / b

def calculate():
    result = divide(10, 0)
    print(result)

def main():
    calculate()

main()

When we execute this script, we get the following stack trace:

Traceback (most recent call last):
  File "/tmp/example.py", line 11, in <module>
    main()
  File "/tmp/example.py", line 9, in main
    calculate()
  File "/tmp/example.py", line 5, in calculate
    result = divide(10, 0)
             ^^^^^^^^^^^^^
  File "/tmp/example.py", line 2, in divide
    return a / b
           ~~^~~
ZeroDivisionError: division by zero

As we can see, this stack trace shows a sequence of function calls. For each one, we have a file name, line number, and function or module name, as well as the line of code itself.

Reading from top to bottom, this stack trace tells the following story:

  1. The main() function was called on line 11 of the example.py script.
  2. Inside main, calculate() was called on line 9.
  3. Inside calculate, divide(10, 0) was called on line 5, with the intention of storing its return value in result. The ^ characters indicate that the exception occurred inside divide, before it could return a value.
  4. Inside divide, an attempt was made to execute a / b, which resolved to 10 / 0. The ^ character indicates that the exception resulted from the division operation.

If we were to step through this script with a debugger, we would stop at each point detailed above. You can try this by running the following command to open the script in Python’s default debugger, pdb:

python -m pdb example.py

In the shell that appears, use the command s (or step) to repeatedly step through the code until the exception is encountered.

As this is a simple script, our stack trace is also quite simple and only includes code from the script itself. A more complex script, with one or more imported modules, would produce longer stack traces. Depending on the code path that resulted in the exception, these stack traces may include substantial portions of code detailing the sequences of function calls within imported modules. We can read those stack traces in the same way, but it usually saves time to skim past the lines covering our code’s dependencies, assuming we trust them to be substantially bug-free.

For example, here’s a simple script that uses the requests library:

import requests

def retrieve(url):
    requests.get(url)

url = "ABC"
print("Retrieving contents of URL...")
retrieve(url)

When we execute this script, we get the following output:

Retrieving contents of URL...
Traceback (most recent call last):
  File "/tmp/req.py", line 8, in <module>
    retrieve(url)
  File "/tmp/req.py", line 4, in retrieve
    requests.get(url)
  File "/usr/lib/python3.12/site-packages/requests/api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/requests/api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/requests/sessions.py", line 575, in request
    prep = self.prepare_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/requests/sessions.py", line 484, in prepare_request
    p.prepare(
  File "/usr/lib/python3.12/site-packages/requests/models.py", line 367, in prepare
    self.prepare_url(url, params)
  File "/usr/lib/python3.12/site-packages/requests/models.py", line 438, in prepare_url
    raise MissingSchema(
requests.exceptions.MissingSchema: Invalid URL 'ABC': No scheme supplied. Perhaps you meant https://ABC?

Only the first two lines of this stack trace show our own code. The rest of the stack trace shows the execution path through the requests library. If, as will usually be the case, we don’t have any intention of modifying the requests library, we can safely ignore these lines in the stack trace, and skip to the error message at the bottom, which informs us that our URL was incorrectly formatted.

To demonstrate a case where more than one stack trace is shown, let’s add some faulty error handling to our division-by-zero script from before.

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return a / c

def calculate():
    result = divide(10, 0)
    print(result)

def main():
    calculate()

main()

When this script is run, we should receive the following output:

Traceback (most recent call last):
  File "/tmp/example.py", line 3, in divide
    return a / b
           ~~^~~
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/tmp/example.py", line 14, in <module>
    main()
  File "/tmp/example.py", line 12, in main
    calculate()
  File "/tmp/example.py", line 8, in calculate
    result = divide(10, 0)
             ^^^^^^^^^^^^^
  File "/tmp/example.py", line 5, in divide
    return a / c
               ^
NameError: name 'c' is not defined

Here, we see two stack traces:

  1. The first is a truncated stack trace, showing the initial ZeroDivisionError.
  2. The second is a full stack trace, showing a NameError that occurred while handling the first exception.

When debugging this code, we should address the second error (NameError) first. Once that’s done, we can rerun the code to get a full stack trace for the ZeroDivisionError, as we did initially.

For more about working with stack traces, see this answer on printing stack traces in Python.

Considered "not bad" by 4 million developers and more than 150,000 organizations worldwide, Sentry provides code-level observability to many of the world's best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.

Sentry