Skip to content

Allow constructing PyTraceback - #5857

Merged
bschoenmaeckers merged 12 commits into
PyO3:mainfrom
bschoenmaeckers:construct-traceback
Mar 13, 2026
Merged

bschoenmaeckers merged 12 commits into
PyO3:mainfrom
bschoenmaeckers:construct-traceback

Conversation

@bschoenmaeckers

Copy link
Copy Markdown
Member

No description provided.

Comment thread src/types/traceback.rs Outdated
Comment thread src/types/traceback.rs Outdated
@bschoenmaeckers
bschoenmaeckers force-pushed the construct-traceback branch 2 times, most recently from 911f992 to 5096f1f Compare March 6, 2026 09:15
@davidhewitt

Copy link
Copy Markdown
Member

Interesting, I would be very curious to know how this looks for injecting Rust call frames into Python tracebacks.

@bschoenmaeckers

Copy link
Copy Markdown
Member Author

Interesting, I would be very curious to know how this looks for injecting Rust call frames into Python tracebacks.

Did some testing usingbtparse:

Traceback (most recent call last):
  File "<unknown>", line 0, in __pthread_cond_wait
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/sys/thread/unix.rs", line 127, in std::sys::thread::unix::Thread::new::thread_start
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/alloc/src/boxed.rs", line 2206, in <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/core/src/ops/function.rs", line 250, in core::ops::function::FnOnce::call_once{{vtable.shim}}
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/thread/lifecycle.rs", line 89, in std::thread::lifecycle::spawn_unchecked::{{closure}}
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/panic.rs", line 359, in std::panic::catch_unwind
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/panicking.rs", line 544, in std::panicking::catch_unwind
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/panicking.rs", line 581, in std::panicking::catch_unwind::do_call
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/core/src/panic/unwind_safe.rs", line 274, in <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/thread/lifecycle.rs", line 91, in std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/sys/backtrace.rs", line 166, in std::sys::backtrace::__rust_begin_short_backtrace
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/test/src/lib.rs", line 637, in test::run_test::{{closure}}
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/test/src/lib.rs", line 607, in test::run_test::{{closure}}
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/test/src/lib.rs", line 686, in test::run_test_in_process
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/panic.rs", line 359, in std::panic::catch_unwind
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/panicking.rs", line 544, in std::panicking::catch_unwind
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/panicking.rs", line 581, in std::panicking::catch_unwind::do_call
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/core/src/panic/unwind_safe.rs", line 274, in <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/test/src/lib.rs", line 686, in test::run_test_in_process::{{closure}}
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/test/src/lib.rs", line 663, in test::__rust_begin_short_backtrace
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/core/src/ops/function.rs", line 250, in core::ops::function::FnOnce::call_once
  File "/Users/basschoenmaeckers/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs", line 250, in core::ops::function::FnOnce::call_once
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
  File "./src/types/traceback.rs", line 230, in pyo3::types::traceback::tests::test_convert_backtrace::{{closure}}
    fn test_convert_backtrace() {
  File "./src/types/traceback.rs", line 231, in pyo3::types::traceback::tests::test_convert_backtrace
    Python::attach(|py| {
  File "./src/marker.rs", line 415, in pyo3::marker::Python::attach
    f(guard.python())
  File "./src/types/traceback.rs", line 232, in pyo3::types::traceback::tests::test_convert_backtrace::{{closure}}
    let backtrace = Backtrace::force_capture();
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/backtrace.rs", line 331, in std::backtrace::Backtrace::create
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/../../backtrace/src/backtrace/mod.rs", line 66, in std::backtrace_rs::backtrace::trace_unsynchronized
  File "/rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/../../backtrace/src/backtrace/libunwind.rs", line 117, in std::backtrace_rs::backtrace::libunwind::trace

@bschoenmaeckers
bschoenmaeckers marked this pull request as ready for review March 6, 2026 10:50
Comment thread src/types/traceback.rs Outdated
Comment thread src/types/traceback.rs Outdated
Comment on lines +60 to +63
.unwrap_or_else(|| {
let frame = PyFrame::new(py, c"<unknown>", c"<unknown>", 0)?;
PyTraceback::new(py, None, frame, 0, 0)
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than a null frame, should this just result Result<Option<...>> and let the user handle the empty case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me.

@bschoenmaeckers

Copy link
Copy Markdown
Member Author

Interesting, I would be very curious to know how this looks for injecting Rust call frames into Python tracebacks.

We could experiment with creating PyFrame from std::panic::Location::caller and adding that to the current exception traceback. 🤔

@bschoenmaeckers

Copy link
Copy Markdown
Member Author

Might be nice to find a way to do this in abi3 as well. Does someone have an idea how to do that?

@Icxolu

Icxolu commented Mar 7, 2026

Copy link
Copy Markdown
Member

PyTrackback is available in types module. You could call into Python to construct it on the limited abi. I don't think it's possible to create a frame object from the Python side, but if you happen to have one you could create a traceback object this way.

@davidhewitt

Copy link
Copy Markdown
Member

We could potentially open an upstream CPython issue asking for a way to create dummy Rust tracebacks in the stable ABI; this sort of mixed-language traceback might be a nice ergonomics win for some projects.

On the other hand, perhaps debugging the Rust frames is most useful when debugging the Rust module itself, at which time it won't be for distribution so using the limited API might be acceptable...

@bschoenmaeckers

Copy link
Copy Markdown
Member Author

PyTrackback is available in types module. You could call into Python to construct it on the limited abi. I don't think it's possible to create a frame object from the Python side, but if you happen to have one you could create a traceback object this way.

I could not find a way to construct a frame with python, so might not be possible.

>>> types.FrameType()
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    types.FrameType()
    ~~~~~~~~~~~~~~~^^
TypeError: cannot create 'frame' instances

On the other hand, perhaps debugging the Rust frames is most useful when debugging the Rust module itself, at which time it won't be for distribution so using the limited API might be acceptable...

I think for now this is acceptable as a first step.

@bschoenmaeckers

Copy link
Copy Markdown
Member Author

Found a way to make this work on abi3, but it is not going to make you happy. So I will leave it out for now.

    let code = PyCode::compile(py, c"raise Exception()", file_name, PyCodeInput::File)?;
    let kwargs = [
        ("co_firstlineno", line_number.into_py_any(py)?),
        ("co_name", func_name.into_py_any(py)?),
    ]
    .into_py_dict(py)?;

    let traceback = code
        .call_method("replace", (), Some(&kwargs))
        .cast_into::<PyCode>()?
        .run(None, None)
        .unwrap_err()
        .traceback(py)
        .expect("traceback should be present");

    traceback.getattr("tb_frame").cast_into()

Comment thread src/types/traceback.rs Outdated
Comment on lines +44 to +63
/// Creates a new traceback object from an iterator of frames.
///
/// The frames should be ordered from newest to oldest, i.e. the first frame in the iterator
/// will be the innermost frame in the traceback.
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
pub fn from_frames<'py, I>(
py: Python<'py>,
frames: I,
) -> PyResult<Option<Bound<'py, PyTraceback>>>
where
I: IntoIterator,
I::Item: IntoPyObject<'py,>
{
frames.into_iter().try_fold(None, |prev, frame| {
let frame = frame.into_pyobject(py).map_err(Into::into)?.into_bound();
let line_number = frame.line_number();
PyTraceback::new(py, prev, frame, 0, line_number).map(Some)
})
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this api yet. Should I remove it from this MR so we can iterate on the design later?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed PyTraceback::from_frames for now. So this is ready for final review.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me to play it slow 👍

@davidhewitt davidhewitt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this seems good to me!

Comment thread newsfragments/5857.added.md Outdated
Comment thread src/types/frame.rs Outdated
@bschoenmaeckers
bschoenmaeckers added this pull request to the merge queue Mar 13, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 13, 2026
@bschoenmaeckers
bschoenmaeckers added this pull request to the merge queue Mar 13, 2026
Merged via the queue into PyO3:main with commit 7aa6163 Mar 13, 2026
45 checks passed
@bschoenmaeckers
bschoenmaeckers deleted the construct-traceback branch March 13, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants