The other day, when I was updating the source code with the project running, I noticed that the exception stack trace printed is not right, i.e., the printed error line is actually not the line that is triggering the exception.

Why did this happen? Is it related to the modification to the source file? At first, I thought it absurd. Based on my knowledge, when Python runs the source code, it will load it into memory. Since it is executing the code in the memory, so it should print the offending line that is kept in the memory.

To figure it out, I write the following code (test.py):

import time

time.sleep(10)
print(1/0)


I run it (python test.py), then add something below the time statement:

import time

time.sleep(10)
# some comment
print(1/0)


Then program reaches the print statement and printed the following exception:

Traceback (most recent call last):
File "test.py", line 4, in <module>
# some comment
ZeroDivisionError: division by zero


Surprisingly, the stack trace printed is the 4th line in the modified test.py, not the original 4th line (print(1/0)).

So my perception of how Python runs its code is not fully correct. I searched on the internet and found some explanation about this, for example, discussion here.

When Python run a source code, it will first compile the source code into the so called bytecode. Then the bytecode is executed by the Python virtual machine.

So what is really running is the bytecode, not the original source code. We can use the dis package in standard library to inspect the bytecode produced by Python.

import dis

my_str = "import time\n\ntime.sleep(10)\nprint(1/0)"
print(dis.dis(my_str))


It will show the bytecode along with other infos for the source code:

│  1           0 LOAD_CONST               0 (0)
│              4 IMPORT_NAME              0 (time)
│              6 STORE_NAME               0 (time)
│
│  3           8 LOAD_NAME                0 (time)
│             14 CALL_METHOD              1
│             16 POP_TOP
│
│  4          18 LOAD_NAME                2 (print)
│             24 BINARY_TRUE_DIVIDE
│             26 CALL_FUNCTION            1
│             28 POP_TOP

Number in the first column corresponds to the code line in the original script. For example, the first line (import time) is turned into 4 bytecode instructions.