Skip to content

Unexpected location of naked exception wrapped in except* block #128799

Closed

Description

@jobh

Bug report

Bug description:

When an ExceptionGroup is implicitly constructed by catching a naked exception in an except* block, the traceback of the ExceptionGroup seems to point to the stackframe above where it is logically created:

def f():
    try:
        raise Exception  # Exception (inner) traceback terminates here
    except* Exception as e:
        raise

f()  # ExceptionGroup (wrapper) traceback terminates here

→

  + Exception Group Traceback (most recent call last):
  |   File "/home/jobh/src/hypothesis/test_naked_exception.py", line 7, in <module>
  |     f()  # ExceptionGroup (wrapper) traceback terminates here
  |     ^^^
  | ExceptionGroup:  (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/home/jobh/src/hypothesis/test_naked_exception.py", line 3, in f
    |     raise Exception  # Exception (inner) traceback terminates here
    |     ^^^^^^^^^^^^^^^
    | Exception
    +------------------------------------

I'm not sure if this should be called a bug, as it's not specifically documented how the wrapping ExceptionGroup traceback is constructed. But it is surprising, and did cause a bug in hypothesis where the Exception traceback is used to detect whether an exception originates in hypothesis itself or in user code 12. The issue being that for wrapped naked exceptions we have to inspect the inner exception since the outer exception appears to be raised by the calling function.

As an alternative to the current behaviour, would it be possible to create the wrapper ExceptionGroup with a traceback pointing to where it is logically triggered - i.e., the except* block - rather than initially empty? IMO, that would be less suprising and would allow us to use the traceback without special-casing ExceptionGroup.

([edit] Or, if the desire is to avoid branching the traceback between the wrapper and the inner exception, use the outermost frame of the naked exception as the initial ExceptionGroup traceback. This would still avoid the appearance of happening in the caller.)

CPython versions tested on:

3.11, 3.12, 3.13

Operating systems tested on:

Linux

Linked PRs

Footnotes

  1. https://github.com/HypothesisWorks/hypothesis/issues/4183 ↩

  2. https://github.com/HypothesisWorks/hypothesis/pull/4239 ↩

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Jan 13, 2025
  2. picnixz commented on Jan 14, 2025

    @picnixz
    Member

    cc @iritkatriel @1st1 @gvanrossum as the authors of PEP-654.

  3. iritkatriel commented on Jan 14, 2025

    @iritkatriel
    Member

    I think it's fine to add a frame in the traceback for the except Exception* line. It would be clearer than making it look like the raise Exception line created an ExceptionGroup..

  4. self-assigned this
    on Jan 15, 2025
  5. added a commit that references this issue on Jan 18, 2025
  6. iritkatriel commented on Jan 18, 2025

    @iritkatriel
    Member

    PR to fix it: #128971

    I would call this a bug and backport. @gvanrossum do you agree?

  7. iritkatriel commented on Jan 18, 2025

    @iritkatriel
    Member

    With the PR:

    >>> def f():
    ...     try:
    ...         raise Exception  # Exception (inner) traceback terminates here
    ...     except* Exception as e:
    ...         raise
    ... 
    ... f()  # ExceptionGroup (wrapper) traceback terminates here
    ... 
      + Exception Group Traceback (most recent call last):
      |   File "<python-input-0>", line 7, in <module>
      |     f()  # ExceptionGroup (wrapper) traceback terminates here
      |     ~^^
      |   File "<python-input-0>", line 4, in f
      |     except* Exception as e:
      |         raise
      | ExceptionGroup:  (1 sub-exception)
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File "<python-input-0>", line 3, in f
        |     raise Exception  # Exception (inner) traceback terminates here
        |     ^^^^^^^^^^^^^^^
        | Exception
        +------------------------------------
    >>> 
    
  8. gvanrossum-ms commented on Jan 18, 2025

    @gvanrossum-ms
    Contributor

    Why does the raise on line 5 show up in the traceback?

  9. iritkatriel commented on Jan 18, 2025

    @iritkatriel
    Member

    Why does the raise on line 5 show up in the traceback?

    Good catch. It's because the location info for the except* in the code object spans the whole block. I'll need to fix that:

    >>> def f():
    ...     try:
    ...         raise Exception  # Exception (inner) traceback terminates here
    ...     except* Exception as e:
    ...         1
    ...         2
    ...         3
    ...         raise
    ... 
    ... f()  # ExceptionGroup (wrapper) traceback terminates here
    ... 
      + Exception Group Traceback (most recent call last):
      |   File "<python-input-3>", line 10, in <module>
      |     f()  # ExceptionGroup (wrapper) traceback terminates here
      |     ~^^
      |   File "<python-input-3>", line 4, in f
      |     except* Exception as e:
      |     ...<3 lines>...
      |         raise
      | ExceptionGroup:  (1 sub-exception)
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File "<python-input-3>", line 3, in f
        |     raise Exception  # Exception (inner) traceback terminates here
        |     ^^^^^^^^^^^^^^^
        | Exception
        +------------------------------------
    
    
  10. iritkatriel commented on Jan 19, 2025

    @iritkatriel
    Member

    Good catch. It's because the location info for the except* in the code object spans the whole block. I'll need to fix that:

    This is a fix for the location info: #129026

  11. iritkatriel commented on Jan 22, 2025

    @iritkatriel
    Member

    This is a fix for the location info: #129026

    There is now also an AST issue to add more precise locations for compound statements: #129157

    I don't think that should hold off merging the fix for this issue. It's an orthogonal problem to the one presented here.

  12. added 6 commits that reference this issue on Jan 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions