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 — line number for a return in a with block after 3.10 is that of the with, not the return statement · Issue #93975 · python/cpython

line number for a return in a with block after 3.10 is that of the with, not the return statement #93975

New issue
Copy link
New issue
Copy link
Closed as not planned
Closed as not planned
line number for a return in a with block after 3.10 is that of the with, not the return statement#93975
Copy link
Assignees
markshannon
Labels
3.10 (EOL)end of lifeend of life3.11only security fixesonly security fixes3.12only security fixesonly security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@gpshead
gpshead
opened on Jun 18, 2022
Issue body actions
def foo() -> int:
    with open('/dev/null') as devnull:
        return 'a'

In Python 3.10 onwards, the reported line number for the return statement is incorrect.

Python main 3.12-main-ish not far from 3.11:

>>> def foo() -> int:
...   with open('a') as f:
...     return 'b'
... 
>>> dis.dis(foo)
  1           0 RESUME                   0

  2           2 LOAD_GLOBAL              1 (NULL + open)
             14 LOAD_CONST               1 ('a')
             16 CALL                     1
             26 BEFORE_WITH
             28 STORE_FAST               0 (f)

  3          30 NOP

  2          32 LOAD_CONST               0 (None)
             34 LOAD_CONST               0 (None)
             36 LOAD_CONST               0 (None)
             38 CALL                     2
             48 POP_TOP
             50 LOAD_CONST               2 ('b')
             52 RETURN_VALUE
        >>   54 PUSH_EXC_INFO
             56 WITH_EXCEPT_START
             58 POP_JUMP_FORWARD_IF_TRUE     4 (to 68)
             60 RERAISE                  2
        >>   62 COPY                     3
             64 POP_EXCEPT
             66 RERAISE                  1
        >>   68 POP_TOP
             70 POP_EXCEPT
             72 POP_TOP
             74 POP_TOP
             76 LOAD_CONST               0 (None)
             78 RETURN_VALUE
ExceptionTable:
  28 to 30 -> 54 [1] lasti
  54 to 60 -> 62 [3] lasti
  68 to 68 -> 62 [3] lasti
>>> sys.version_info
sys.version_info(major=3, minor=12, micro=0, releaselevel='alpha', serial=0)

The only thing attributed to line 3 is a NOP. The RETURN_VALUE of 'b' is listed as line 2. That is wrong.

Python 3.9:

>>> def foo() -> int:
...   with open('a') as f:
...     return 'b'
... 
>>> dis.dis(foo)
  2           0 LOAD_GLOBAL              0 (open)
              2 LOAD_CONST               1 ('a')
              4 CALL_FUNCTION            1
              6 SETUP_WITH              18 (to 26)
              8 STORE_FAST               0 (f)

  3          10 POP_BLOCK
             12 LOAD_CONST               0 (None)
             14 DUP_TOP
             16 DUP_TOP
             18 CALL_FUNCTION            3
             20 POP_TOP
             22 LOAD_CONST               2 ('b')
             24 RETURN_VALUE
        >>   26 WITH_EXCEPT_START
             28 POP_JUMP_IF_TRUE        32
             30 RERAISE
        >>   32 POP_TOP
             34 POP_TOP
             36 POP_TOP
             38 POP_EXCEPT
             40 POP_TOP
             42 LOAD_CONST               0 (None)
             44 RETURN_VALUE
>>> sys.version_info
sys.version_info(major=3, minor=9, micro=12, releaselevel='final', serial=0)

Noticed by @martindemello working on a 3.10 error attribution bug in pytype.

Activity

  1. gpshead
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    3.11only security fixesonly security fixes
    3.10 (EOL)end of lifeend of life
    3.12only security fixesonly security fixes
    on Jun 18, 2022
  2. gpshead
    assigned
    markshannon
    on Jun 18, 2022
  3. dignissimus commented on Jun 18, 2022

    @dignissimus
    dignissimus
    on Jun 18, 2022
    Contributor
    More actions

    The issue was introduced in 937cebc
    See bpo-44298: #88464

    The same behaviour occurs with try-finally blocks on both 3.9 and 3.10+

    def function():
        try:
            return
        finally:
            pass

    With the above code, disassembling on Python 3.10.5 produces the following

      1           0 LOAD_CONST               0 (<code object function at 0x7f96b4e0d000, file "a.py", line 1>)
                  2 LOAD_CONST               1 ('function')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (function)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    
    Disassembly of <code object function at 0x7f96b4e0d000, file "tryfinally.py", line 1>:
      2           0 SETUP_FINALLY            3 (to 8)
    
      3           2 POP_BLOCK
    
      5           4 LOAD_CONST               0 (None)
                  6 RETURN_VALUE
            >>    8 RERAISE                  0

    And on 3.9.0

      1           0 LOAD_CONST               0 (<code object function at 0x7f72158a8be0, file "/tmp/a.py", line 1>)
                  2 LOAD_CONST               1 ('function')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (function)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    
    Disassembly of <code object function at 0x7f72158a8be0, file "tryfinally.py", line 1>:
      2           0 SETUP_FINALLY            6 (to 8)
    
      3           2 POP_BLOCK
                  4 LOAD_CONST               0 (None)
                  6 RETURN_VALUE
    
      5     >>    8 RERAISE
                 10 LOAD_CONST               0 (None)
                 12 RETURN_VALUE
  4. miss-islington
    added a commit that references this issue on Jun 18, 2022

    gh-93975: Nicer error reporting in test_venv (GH-93959)

    6066f45
  5. jaraco
    added 3 commits that reference this issue on Jun 19, 2022

    [3.11] gh-93975: Nicer error reporting in test_venv (GH-93959)

    ff79876

    [3.10] gh-93975: Nicer error reporting in test_venv (GH-93959)

    dd4016e

    [3.9] gh-93975: Nicer error reporting in test_venv (GH-93959)

    3f5b7ac
  6. markshannon commented on Jun 20, 2022

    @markshannon
    markshannon
    on Jun 20, 2022
    Member
    More actions

    I believe the current behavior is correct.

    def foo() -> int:
        with open('/dev/null') as devnull:
            return 'a'

    The sequence of events is:

    • Function start at line 1
    • Line 2
    • Evaluate open and enter the with block
    • Line 3
    • Evaluate 'a' and the return statement
    • Line 2
    • Exit the with block

    The jump back to line 2 may seem surprising, but it is necessary.
    Because the call to __exit__ when exiting the with statement is is observable and may raise an exception, it needs to have a line number.

  7. markshannon commented on Jun 20, 2022

    @markshannon
    markshannon
    on Jun 20, 2022
    Member
    More actions

    If you change "a" to len("a"), note that len("a") is on line 3.

    def foo() -> int:
        with open('/dev/null') as devnull:
            return len("a")
      1           0 RESUME                   0
    
      2           2 LOAD_GLOBAL              1 (NULL + open)
                 14 LOAD_CONST               1 ('/dev/null')
                 16 CALL                     1
                 26 BEFORE_WITH
                 28 STORE_FAST               0 (devnull)
    
      3          30 LOAD_GLOBAL              3 (NULL + len)
                 42 LOAD_CONST               2 ('a')
                 44 CALL                     1
    
      2          54 SWAP                     2
                 56 LOAD_CONST               0 (None)
                 58 LOAD_CONST               0 (None)
                 60 LOAD_CONST               0 (None)
                 62 CALL                     2
                 72 POP_TOP
                 74 RETURN_VALUE
            >>   76 PUSH_EXC_INFO
                 78 WITH_EXCEPT_START
                 80 POP_JUMP_FORWARD_IF_TRUE     1 (to 84)
                 82 RERAISE                  2
            >>   84 POP_TOP
                 86 POP_EXCEPT
                 88 POP_TOP
                 90 POP_TOP
                 92 LOAD_CONST               0 (None)
                 94 RETURN_VALUE
            >>   96 COPY                     3
                 98 POP_EXCEPT
                100 RERAISE                  1
    ExceptionTable:
      28 to 52 -> 76 [1] lasti
      76 to 84 -> 96 [3] lasti
    
  8. markshannon commented on Jun 20, 2022

    @markshannon
    markshannon
    on Jun 20, 2022
    Member
    More actions

    On more thing.
    In Python 3.9, if __exit__ raises, it reports the wrong line number.

    def foo() -> int:
        with raises_on_exit() as mgr:
            return 'a'
            pass

    Will report the exception on line 4, which is clearly wrong.

    It is 3.9 that is broken, not 3.10 or 3.11, IMO.

  9. jaraco commented on Jun 20, 2022

    @jaraco
    jaraco
    on Jun 20, 2022
    Member
    More actions

    Please disregard the commits above referencing this issue. They should have referenced #93957.

    Reacted by Gregory P. Smith
  10. gpshead commented on Jun 20, 2022

    @gpshead
    gpshead
    on Jun 20, 2022
    MemberAuthor
    More actions

    Thanks Mark! From a bytecode perspective I can understand how this might not be a bug, especially as our bytecode becomes more optimal.

    If this is a non-issue feel free to close it.

    I believe the question from a pytype perspective, which does its program analysis via bytecode, is if it is possible to determine where the return statement was in the code for error reporting reasons. '' not being an int, the return statement attempting the wrong type is where an error message would ideally point. In absense of concrete information from the bytecode the analysis could possibly detect that a with or try block is being exited and perhaps tell the user the more vague block the errant return was exiting from. I'll leave that up to pytype folks to figure out.

  11. markshannon commented on Jun 20, 2022

    @markshannon
    markshannon
    on Jun 20, 2022
    Member
    More actions

    If the expression in the return statement is anything but a constant, it should have the exact location information, not just the line number. If it is a constant, then simple AST analysis will do.

    My advice to the pytype developers would be to build their own CFG. The transformations required for precise type analysis and for code generation are different enough that a custom CFG is probably the way to go.

    Here's one I made earier 🙂
    https://codeql.github.com/docs/codeql-language-guides/analyzing-control-flow-in-python/

  12. markshannon commented on Jun 20, 2022

    @markshannon
    markshannon
    on Jun 20, 2022
    Member
    More actions

    OK, I'll close this.

  13. markshannon
    closed this as not plannedon Jun 20, 2022
  14. serhiy-storchaka
    added a commit that references this issue on Jun 26, 2022

    Revert 30935 defer bpo45162 to 312 (#94285)

    1ffde3e
  15. ambv
    added 2 commits that reference this issue on Jul 1, 2022

    [3.11] gh-93975: Provide nicer error reporting from subprocesses in t…

    c4f82ea

    [3.10] gh-93975: Provide nicer error reporting from subprocesses in t…

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

Metadata

Metadata

Assignees

  • @markshannon
    markshannon

Labels

3.10 (EOL)end of lifeend of life3.11only security fixesonly security fixes3.12only security fixesonly security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-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.