Consider these two examples:
x = 1; y = 2; z = 3
and:
for i in range(3): print(i)
In the latter, if you step through this in a debugger like pdb, you'll it will stop at the print(i) at each iteration of a loop.
However in the first example, it stops once.
Investigating further, disassembling the multi-statement line we see that there are in fact two entries for the first line in co_lnotab. But dis.dis() lies about this.
As for the for loop there is just the one line in the lnotab but the place that you stop at on each interation, offset 10, is at the target of a jump. So is that what triggers stopping even though the line number hasn't changed?
import dis
>>> x = compile('x = 1; y = 2; z = 3', 'foo', 'exec')
>>> x.co_lnotab
b'\x04\x00\x04\x00'
>>> dis.dis(x)
1 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (x)
4 LOAD_CONST 1 (2)
6 STORE_NAME 1 (y)
8 LOAD_CONST 2 (3)
10 STORE_NAME 2 (z)
12 LOAD_CONST 3 (4)
14 STORE_NAME 3 (a)
16 LOAD_CONST 4 (None)
18 RETURN_VALUE
>>> y = compile('for i in range(3): print(i)', 'foo', 'exec')
>>> y.co_lnotab
b'\x0e\x00'
>>> dis.dis(y)
1 0 SETUP_LOOP 24 (to 26)
2 LOAD_NAME 0 (range)
4 LOAD_CONST 0 (3)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24)
12 STORE_NAME 1 (i)
14 LOAD_NAME 2 (print)
16 LOAD_NAME 1 (i)
18 CALL_FUNCTION 1
20 POP_TOP
22 JUMP_ABSOLUTE 10
>> 24 POP_BLOCK
>> 26 LOAD_CONST 1 (None)
28 RETURN_VALUE
>>>
Where is the source code for this logic? I've looked in the Python C code, but I can't find it, say in ceval.c looking for PyTrace_LINE.
Edit:
Based on user2357112's answer and reading the code suggested there, I was able to verify that one can stop/trace on each statement of the code. I used my new Python assembler, pyc-xasm, to modify the bytecode to this:
2:
LOAD_CONST (1)
STORE_NAME (x)
JUMP_FORWARD L2B
L2A:
2:
LOAD_CONST (2)
STORE_NAME (y)
JUMP_FORWARD L2D
L2B:
JUMP_ABSOLUTE L2A
L2C:
2:
LOAD_CONST (3)
STORE_NAME (z)
JUMP_FORWARD L3
L2D:
JUMP_ABSOLUTE L2C
L3:
3:
LOAD_NAME (x)
LOAD_NAME (y)
BINARY_ADD
LOAD_NAME (z)
BINARY_ADD
PRINT_ITEM
PRINT_NEWLINE
LOAD_CONST (None)
RETURN_VALUE
And running this will cause Python to stop before each line.