Monday, January 3, 2022

Python Morsels: Reading tracebacks in Python

When Python encounters an error in your code, it will print out a traceback. Let's talk about how to use tracebacks to fix our code.

What are tracebacks?

Here we have a program called count.py:

import sys

def count_to(number):
    for n in range(1, number+1):
        print(n)

def main():
    stop = sys.argv[1]
    count_to(stop)

if __name__ == "__main__":
    main()

This program is supposed to accept a number and print out all the numbers up to and including that number.

But this code is broken. When we run it, it prints out a traceback:

$ python3 count.py 5
Traceback (most recent call last):
  File "/home/trey/count.py", line 12, in <module>
    main()
  File "/home/trey/count.py", line 9, in main
    count_to(stop)
  File "/home/trey/count.py", line 4, in count_to
    for n in range(1, number+1):
TypeError: can only concatenate str (not "int") to str

When Python encounters an exception that isn't handled in your code, it will print out a traceback.

Read the last line in a traceback first

Tracebacks are supposed to be read from the bottom upward: the very last line in a traceback is the first line that you're supposed to read.

That last line in the traceback describes the type of exception that occurred, and it shows us an error message that's supposed to explain what happened:

TypeError: can only concatenate str (not "int") to str

The error message shown in tracebacks can sometimes be a little bit cryptic, but it's typically better than nothing.

After that last line, we're supposed to read the two lines above that:

  File "/home/trey/count.py", line 4, in count_to
    for n in range(1, number+1):

And then the two lines above that:

  File "/home/trey/count.py", line 9, in main
    count_to(stop)

And so on.

We read tracebacks from the bottom upward.

The two lines above that last one describe where we actually were in our code when the exception occurred:

  File "/home/trey/count.py", line 4, in count_to
    for n in range(1, number+1):

We were in count.py, on line 4, in the count_to function (note that you can see the actual line of code in that file as well).

The lines in a traceback represent frames in the "call stack"

Python uses something called a call stack to keep track of where you are in your Python process at any one time. It keeps track of where you're supposed to go next when you return from your current function, where that function will return to, and so on.

The lines in our traceback describe our call stack. That is, they describe where Python actually was in our process when the exception occurred:

  File "/home/trey/count.py", line 12, in <module>
    main()
  File "/home/trey/count.py", line 9, in main
    count_to(stop)
  File "/home/trey/count.py", line 4, in count_to
    for n in range(1, number+1):

Each of those sets of two lines are called stack frames. You can think of Stack frames as levels of depth in our code: the bottom-most two lines in our traceback represents the deepest level, while the top-most lines represent the furthest level out from where that exception occurred.

So in our count.py program, line 4 is where the exception actually occurred:

  File "/home/trey/count.py", line 4, in count_to
    for n in range(1, number+1):

Because tracebacks represent lines in the call stack, they're also sometimes called a stack trace.

In general, the deepest stack frame in our traceback won't always be our code (that is code we wrote). It could be code from some other module besides our own. So sometimes you'll need to read other frames in your call stack above that bottom one.

Reading up the call stack to fix the bug

Now, I happen to know that what this TypeError means:

TypeError: can only concatenate str (not "int") to str

That error message means Python tried to use the + operator between a string and an integer, and that's not allowed. The exception occurred on line 4, in number+1:

import sys

def count_to(number):
    for n in range(1, number+1):
        print(n)

The number variable must be pointing to a string (since 1 is an integer).

We probably shouldn't convert number to an integer in this function, because this function isn't supposed to accept strings. Let's go up to the next frame in our call stack!

The next frame in our call stack here is on line 9:

  File "/home/trey/count.py", line 9, in main
    count_to(stop)
  File "/home/trey/count.py", line 4, in count_to
    for n in range(1, number+1):

This function is where we could fix this error:

def main():
    stop = sys.argv[1]
    count_to(stop)

The line just above count_to(stop), line 8, is where we should convert that string to a number. The command line argument (sys.argv[1]) is coming in as a string, so we should convert it to a number (by passing it to the int function) before pointing the stop variable to it:

def main():
    stop = int(sys.argv[1])
    count_to(stop)

This should fix the bug in our code. Let's try and run our count.py program again:

$ python3 count.py 5
1
2
3
4
5

It works! We've just fixed the bug.

Summary

Tracebacks happen all the time. Reading tracebacks is just part of the process of writing Python code. Whenever Python encounters an unhandled exception in your code, it'll print out a traceback.

Remember, tracebacks should be read from the bottom upward. The last line tells you the exception that occurred and a (hopefully helpful) error message. That last line is what you might want to type into your favorite search engine to figure out what might be going on.

Then the two lines above the last one tell you what line the exception actually occurred on. Then from there, each set of two lines describes the next level of depth in our code (a.k.a. stack frames).

Remember: read your tracebacks from the bottom upward.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...