GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Setting `f_trace_opcodes` to `True` can lead to `f_lineno` being removed in some cases (using `breakpoint()`/`pdb.set_trace()`) · Issue #127321 · python/cpython

Setting f_trace_opcodes to True can lead to f_lineno being removed in some cases (using breakpoint()/pdb.set_trace()) #127321

New issue
Copy link
New issue
Copy link
Closed
Closed
Setting f_trace_opcodes to True can lead to f_lineno being removed in some cases (using breakpoint()/pdb.set_trace())#127321
Copy link
Labels
3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@Viicos
Viicos
opened on Nov 27, 2024
Issue body actions

Bug report

Bug description:

Since #118579 (introduced in 3.13), Bdb.set_trace will call Bdb.set_stepinstr instead of Bdb.set_step (on L406):

cpython/Lib/bdb.py

Lines 389 to 407 in 6d3b520

def set_trace(self, frame=None):
"""Start debugging from frame.
If frame is not specified, debugging starts from caller's frame.
"""
sys.settrace(None)
if frame is None:
frame = sys._getframe().f_back
self.reset()
self.enterframe = frame
while frame:
frame.f_trace = self.trace_dispatch
self.botframe = frame
self.frame_trace_lines_opcodes[frame] = (frame.f_trace_lines, frame.f_trace_opcodes)
# We need f_trace_lines == True for the debugger to work
frame.f_trace_lines = True
frame = frame.f_back
self.set_stepinstr()
sys.settrace(self.trace_dispatch)

This ends up setting f_trace_opcodes to True on all the frames of the stack.

This is fine for most use cases, but for some reason, this removes the f_lineno attribute of frames in exotic setups using breakpoint():

any(some_cond for el in it) and breakpoint()
# -> Warning: lineno is None

[1, 2] and breakpoint()
# -> Warning: lineno is None

True and breakpoint()
# Fine, lineno available

I'm using these inline conditions a lot to conditionally add a breakpoint, and not having access to the line number is annoying as many commands (such as list) will fail because they expect frame.f_lineno to not be None (and thus crashes and exits the debugger).

I'm not familiar with opcodes and how this interacts with frames. It is expected for the f_lineno to be lost here?

CPython versions tested on:

3.13, 3.14

Operating systems tested on:

Linux

Linked PRs

  • gh-127321: Avoid stopping at an opcode without an associated line number for breakpoint() #127457
  • [3.13] gh-127321: Avoid stopping at an opcode without an associated line number for breakpoint() (GH-127457) #127487

Activity

  1. Viicos
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Nov 27, 2024
  2. picnixz
    added
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    on Nov 27, 2024
  3. picnixz commented on Nov 27, 2024

    @picnixz
    picnixz
    on Nov 27, 2024
    Member
    More actions

    cc @gaogaotiantian

  4. gaogaotiantian commented on Nov 27, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 27, 2024
    Member
    More actions

    I'm not able to reproduce this on either main or 3.13. Could you make a full example?

    [1, 2] and breakpoint()
    > /home/gaogaotiantian/programs/mycpython/scrabble.py(1)<module>()
    -> [1, 2] and breakpoint()
    (Pdb) list
      1  -> [1, 2] and breakpoint()
    [EOF]
    (Pdb) $_frame.f_lineno
    1
    (Pdb) 
    
  5. Viicos commented on Nov 27, 2024

    @Viicos
    Viicos
    on Nov 27, 2024
    ContributorAuthor
    More actions

    Ok took some time to find how to reproduce, but turns out you need at least one extra statement after the breakpoint call. So something like:

    [1,2] and breakpoint()
    
    a = 1

    should work.

  6. gaogaotiantian commented on Nov 27, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 27, 2024
    Member
    More actions

    Ah, interesting. Worked for me, thanks!

  7. gaogaotiantian commented on Nov 27, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 27, 2024
    Member
    More actions

    @iritkatriel the bytecode of

    [1, 2] and breakpoint()
    a = 2

    is

       0           RESUME                   0
    
       1           LOAD_SMALL_INT           1
                   LOAD_SMALL_INT           2
                   BUILD_LIST               2
                   COPY                     1
                   TO_BOOL
                   POP_JUMP_IF_FALSE        7 (to L1)
                   POP_TOP
                   LOAD_NAME                0 (breakpoint)
                   PUSH_NULL
                   CALL                     0
    
      --   L1:     POP_TOP
    
       2           LOAD_SMALL_INT           2
                   STORE_NAME               1 (a)
                   LOAD_CONST               0 (None)
                   RETURN_VALUE
    

    The label L1 is the short circuit label. Is it expected that the bytecode is not associated with any line number? I mean I can understand that the label itself is probably not generated directly by the code, just want to confirm if this is a desired behavior. If so, I'll need to work on some workaround for pdb.

  8. iritkatriel commented on Nov 27, 2024

    @iritkatriel
    iritkatriel
    on Nov 27, 2024
    Member
    More actions

    The POP_TOP is there because we just compiled an expression (and nothing is consuming its result from the stack). If the POP_TOP was only reachable via fall through, the line number of the previous instruction would propagate to it. But here it's reachable via a jump as well (and it seems we don't check that the jump was on the same line as the fall through, but even if we did this would fail in the case where the boolean expression spans more than one line).

    There are situations (such as return and raise instructions) where we force instructions to have a line number even if it requires making multiple copies of some instructions. But this is not one of those cases. Why is it a problem here that there is no line number?

  9. gaogaotiantian commented on Nov 28, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 28, 2024
    Member
    More actions

    Why is it a problem here that there is no line number?

    Because breakpoint() will stop at the next instruction after CALL. In this case, it would be POP_TOP. pdb relies on the line number for multiple things, for example, show the current line. It will also use it for list command to show source code. It would be helpful to know the line number.

    The workaround could be stop at opcode that has line number, but in this case, the next instruction belongs to the next line, which would break the behavior that breakpoint() will stop at the line of breakpoint().

  10. iritkatriel commented on Nov 28, 2024

    @iritkatriel
    iritkatriel
    on Nov 28, 2024
    Member
    More actions

    Can the breakpoint builtin do something to help know where it is?

  11. gaogaotiantian commented on Nov 28, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 28, 2024
    Member
    More actions

    Well it also affects pdb.set_trace() so we can't only deal with it in breakpoint(). If assigning a line number to this opcode is a no-go, we probably have 3 ways to deal with this:

    1. Let it be, it's a corner case anyway.
    2. Make a proxy to "fake" the line number. We can intercept the access to f.f_lineno inside pdb and fake some number. The way I can think of is to get the previous opcode (hopefully CALL) and the line number associated. The downside of this way is that we might have plenty of places that we need to patch and the magic is not pretty. Also when the users access $_frame, they'll see that the lineno is -1, which contradicts what we show them.
    3. Like I said, for set_trace(), stop only when there is a line number. In this case, it will stop at the next line, which is a slight deviation from our existing behavior.

    As this is a rare case, I don't think either of the options above is horrible (although I'm a bit hesitated about 2. as it impacts too much code in pdb). However, from the other point of view, none of the options is perfect either. My perfect solution was for the opcode to have a line number, but maybe that's too much a burden for the compiler.

  12. iritkatriel commented on Nov 28, 2024

    @iritkatriel
    iritkatriel
    on Nov 28, 2024
    Member
    More actions

    My perfect solution was for the opcode to have a line number, but maybe that's too much a burden for the compiler.

    It's not that it's a burden on the compiler. It's that line numbers are static, so if you can reach this NOP from two different paths, you need to make two copies of this NOP, and then add a jump over one of the copies to the next instruction (here it's the LOAD_SMALL_INT). So we emit more code, and add a jump, just for this.

  13. iritkatriel commented on Nov 28, 2024

    @iritkatriel
    iritkatriel
    on Nov 28, 2024
    Member
    More actions

    It might be possible to improve the situation if the compiler does propagate the line number if it's the same for both predecessors (as it is in this example). It won't completely fix the problem, this will still not work:

    >>> (1 or
    ...   2) and breakpoint()
    
  14. gaogaotiantian commented on Nov 28, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 28, 2024
    Member
    More actions

    Yeah I understand the problem behind. I actually have a question here:

    If the compiler knows the return value is not used, why does it need to copy the return value? Can it do a simple POP_JUMP_IF_FALSE? We can save a COPY, TO_BOOL and the extra POP_TOP. (I'm not the expert in bytecodes so I might be completely wrong).

    Also, even if we want a single POP_TOP at the end, do we leave the line number empty? Can we think of it as "pop the eval value from stack after the full expression" so the opcode should be associated to the end of the expression (last line)? I think we have this kind of idea in mind when we deal with loops or other code blocks? I mean in reality, the opcodes we generated are not directly corresponding to an exact line of code, is it better to just leave it blank? Comparing to assigning a reasonable but not completely accurate value? In my mind it gives more useful information to users, even not considering the usage of pdb.

    I think line numbers are majorly used by debuggers, so it's debugging information anyway (correct me if I'm wrong). "I don't know where this opcode is generated" is correct by less useful information for debugging.

  15. iritkatriel commented on Nov 28, 2024

    @iritkatriel
    iritkatriel
    on Nov 28, 2024
    Member
    More actions

    Yeah I understand the problem behind. I actually have a question here:

    If the compiler knows the return value is not used, why does it need to copy the return value? Can it do a simple POP_JUMP_IF_FALSE? We can save a COPY, TO_BOOL and the extra POP_TOP. (I'm not the expert in bytecodes so I might be completely wrong).

    It's a feature of the language that the result of a boolean expression is the value of the element that gave the truthy result:

    >>> x = 0 or 1 or 2
    >>> x
    1
    

    It's common to use this for None handling:

    def f(arg = None):
        arg = arg or DEFAULT_VALUE_OF_ARG
    

    Also, even if we want a single POP_TOP at the end, do we leave the line number empty? Can we think of it as "pop the eval value from stack after the full expression" so the opcode should be associated to the end of the expression (last line)?

    The last line of the expression may not have executed. we want the POP_TOP to have the line number of the last instruction from the expression that executed, otherwise it would mess up the trace.

    I think line numbers are majorly used by debuggers, so it's debugging information anyway (correct me if I'm wrong). "I don't know where this opcode is generated" is correct by less useful information for debugging.

    They are also used for other things, like tracebacks and coverage. Coverage is particularly sensitive to inaccurate line numbers.

  16. gaogaotiantian commented on Nov 29, 2024

    @gaogaotiantian
    gaogaotiantian
    on Nov 29, 2024
    Member
    More actions
    >>> x = 0 or 1 or 2
    >>> x
    1
    

    These are cases where the expressions are assigned to a variable, and this is not causing issue if I understood correctly. I meant for the cases where the expressions are not assigned to anything, so the value is unused. We should be able to optimize that case right? It's faster and it generates less code (and it's rare).

    otherwise it would mess up the trace.

    Hmm okay it might even generate a new line event which is probably something we don't want. I guess I'll have to deal with this in pdb.

  17. gaogaotiantian
    added 2 commits that reference this issue on Dec 1, 2024

    gh-127321: Avoid stopping at an opcode without an associated line num…

    1bc4f07

    [3.13] pythongh-127321: Avoid stopping at an opcode without an associ…

    4f136c9
  18. picnixz
    added
    3.13only security fixesonly security fixes
    3.14bugs and security fixesbugs and security fixes
    on Dec 1, 2024
  19. gaogaotiantian
    added a commit that references this issue on Dec 1, 2024

    [3.13] gh-127321: Avoid stopping at an opcode without an associated l…

    dd2ad70
  20. gaogaotiantian commented on Dec 1, 2024

    @gaogaotiantian
    gaogaotiantian
    on Dec 1, 2024
    Member
    More actions

    This is fixed in main and backported to 3.13.

  21. gaogaotiantian
    closed this as completedon Dec 1, 2024
  22. picnixz
    added a commit that references this issue on Dec 2, 2024

    pythongh-127321: Avoid stopping at an opcode without an associated li…

    854dff2
  23. srinivasreddy
    added a commit that references this issue on Jan 8, 2025

    pythongh-127321: Avoid stopping at an opcode without an associated li…

    e9f9c29
  24. ebonnal
    added a commit that references this issue on Jan 12, 2025

    pythongh-127321: Avoid stopping at an opcode without an associated li…

    34a77ba
  25. vivekchand
    added 2 commits that reference this issue on Mar 11, 2026

    fix: ValueError on Windows Python 3.14 (argparse color detection)

    8b4ab99

    fix: ValueError on Windows Python 3.14 (argparse color detection)

    1630978
  26. appletechie
    added a commit that references this issue on Mar 19, 2026

    fix: ValueError on Windows Python 3.14 (argparse color detection)

    6080cc2
  27. dumko2001
    added a commit that references this issue on Apr 3, 2026

    fix: ValueError on Windows Python 3.14 (argparse color detection)

    7e79165
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.