#966

Traceback loses frames below a try/except or try/finally

Opendsaad68opened 27d ago

When an exception propagates through a frame that has an exception handler (try/except … raise, or try/finally), the traceback loses every frame below that frame. With except … raise, the frame that holds the handler also points at the except line instead of the call that raised.

Reproduced with pydantic-monty 1.0.0, compared with CPython 3.14.8.

Reproducer

def g():
    raise ValueError('boom')

def f():
    try:
        g()
    except BaseException:
        raise

f()

Run with:

import pydantic_monty as pm

with pm.Monty() as pool:
    with pool.checkout(script_name='main.py') as session:
        session.feed_run(CODE)

Monty 1.0.0

Traceback (most recent call last):
  File "<python-input-0>", line 10, in <module>
    f()
    ~~~
  File "<python-input-0>", line 7, in f
    except BaseException:
ValueError: boom

CPython 3.14.8

Traceback (most recent call last):
  File "main.py", line 10, in <module>
    f()
    ~^^
  File "main.py", line 6, in f
    g()
    ~^^
  File "main.py", line 2, in g
    raise ValueError('boom')
ValueError: boom

The g frame is missing, and f's frame shows line 7 (except BaseException:) instead of line 6 (g()).

Variants

  • try/finally (replace the except clause with finally: pass): the g frame is also missing, though f's frame does show line 6 (g()).
  • No handler (f calls g() directly): the traceback is complete and matches CPython, so the frames are lost only when the exception passes through a handler.

Why it matters

Re-raising from a handler and cleanup in finally are common in ordinary code, and the frame that's lost is the one where the error actually happened. In our case (an embedder that runs a program's own modules through exec() inside a prelude), we had to write the prelude without any try/finally around the module calls, so that tracebacks from inside the modules aren't cut.

Expected

As in CPython, an exception that passes through a handler (re-raised bare, or after a finally) keeps the frames it collected below that frame, and each frame keeps the location of its call.

Activity

2
Tiancheng-Xu25d ago

I'd like to work on this. I'll compare traceback propagation with CPython and add regression cases for bare re-raises and finally blocks, preserving the originating frames and call-site locations.

Tiancheng-Xu24d ago

I've implemented traceback preservation for bare re-raises, with regression coverage, in my fork: https://github.com/Tiancheng-Xu/monty/tree/fix/preserve-reraised-traceback

The upstream PR is https://github.com/pydantic/monty/pull/973. It's still under review, and the snapshot-serialization feedback needs targeted verification.