Skip to content

Add bindings for PyDict_SetDefaultRef and use them in PyCode::run#5955

Open
ngoldbaum wants to merge 9 commits intoPyO3:mainfrom
ngoldbaum:globals-set-default
Open

Add bindings for PyDict_SetDefaultRef and use them in PyCode::run#5955
ngoldbaum wants to merge 9 commits intoPyO3:mainfrom
ngoldbaum:globals-set-default

Conversation

@ngoldbaum
Copy link
Copy Markdown
Contributor

@ngoldbaum ngoldbaum commented Apr 6, 2026

While experimenting with a build of PyO3 that supports PEP 803 without using critical sections, I noticed that the use in PyCode::run can be replaced by PyDict_SetDefaultRef. This also makes the implementation a lot simpler.

I added bindings for that function on 3.13 and newer and 3.15 and newer in the limited API. I also added a compat shim based on the implementation in pythoncapi-compat: https://github.com/python/pythoncapi-compat/blob/75a796764d63327268d195e2d5c044e564d0dada/pythoncapi_compat.h#L1314.

@ngoldbaum ngoldbaum force-pushed the globals-set-default branch from 69e4dab to 13a0dc4 Compare April 6, 2026 20:07
@ngoldbaum ngoldbaum marked this pull request as ready for review April 6, 2026 20:33
@ngoldbaum
Copy link
Copy Markdown
Contributor Author

It looks like we don't have extensive coverage tests for our compat shims. Maybe we should? I'd prefer not to add infrastructure for that in this PR though.

Copy link
Copy Markdown
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

Thanks, one thought on the usage of this.

Indeed we don't have full coverage of the compat shims, generally coverage of code in pyo3-ffi is quite low because it's hard to test the FFI code without the convenience layers in pyo3.

We have some tests in src/ffi/tests inside the pyo3 lib, we should probably expand those (maybe warrants an issue?).

For this particular case, maybe we should add setdefault to PyDictMethods? (We could then use that in PyCode::run for bonus safety.)

@ngoldbaum
Copy link
Copy Markdown
Contributor Author

For this particular case, maybe we should add setdefault to PyDictMethods? (We could then use that in PyCode::run for bonus safety.)

Thanks for the suggestion! I added PyDictMethods::{set_default, set_default_with_result}. Please feel free to bikeshed the API I came up with. IMO it makes more sense for the "returns the value" variant to be its own function. I also might push some fixup commits if the CI fails.

Copy link
Copy Markdown
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

Thanks, various suggestions about the new functions

}
}

fn setdefault_result_from_nonerror_return_code(code: std::ffi::c_int) -> bool {
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.

Nit, possibly move this down in the file below the method implementations, not C where we need a forward reference 😉

Comment on lines +223 to +225
/// already present. If an error happens, returns PyErr. This function uses
/// [`PyDict_SetDefaultRef`](https://docs.python.org/3/c-api/dict.html#c.PyDict_SetDefaultRef) internally, so it has
/// the same thread safety guarantees as that function.
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.

Is the thread safety note particularly useful for users? It seems like, following the docs, it's basically. "This function is thread safe, which is generally the case for all the APIs we offer."

}
let py = self.py();

Ok(setdefault_result_from_nonerror_return_code(inner(
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.

I suggest moving the call to setdefault_result_from_nonerror_return_code inside inner - it doesn't depend on the generic parameters.

Comment on lines +480 to +481
static UNINITIALIZED: u8 = 0;
let mut ob_result = std::ptr::addr_of!(UNINITIALIZED) as *mut ffi::PyObject;
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.

One option is to do this with MaybeUninit, though that would forbid being able to assert the call initialized the result.

Suggested change
static UNINITIALIZED: u8 = 0;
let mut ob_result = std::ptr::addr_of!(UNINITIALIZED) as *mut ffi::PyObject;
let mut ob_result = MaybeUninit::new();

.as_borrowed(),
&mut ob_result,
)?;
assert!(ob_result != std::ptr::addr_of!(UNINITIALIZED) as *mut ffi::PyObject);
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.

Is the assert necessary? Seems like this effectively defends a bad implementation of the C API, which is an upstream bug and not our fault if incorrect?

)?;
assert!(ob_result != std::ptr::addr_of!(UNINITIALIZED) as *mut ffi::PyObject);
// SAFETY: the interpreter should have set this to a valid owned PyObject pointer
let out_result = unsafe { ob_result.assume_owned(py) };
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.

Could use assume_owned_unchecked(py) here to avoid a branch, given the safety guarantees.

let py = self.py();
static UNINITIALIZED: u8 = 0;
let mut ob_result = std::ptr::addr_of!(UNINITIALIZED) as *mut ffi::PyObject;
let code = inner(
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.

Similar comment that a lot of the return value handling should be moved into inner as it doesn't depend on the generic parameters.

Comment on lines +1827 to +1829
let res = dict.set_default_with_result("hello", "foobar");
assert!(res.is_ok());
let res = res.unwrap();
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.

Given that the .unwrap() will panic, I'd probably just write this as a one-line

Suggested change
let res = dict.set_default_with_result("hello", "foobar");
assert!(res.is_ok());
let res = res.unwrap();
let (inserted, value) = dict.set_default_with_result("hello", "foobar").unwrap();

assert!(res.is_ok());
let res = res.unwrap();
assert!(!res.0);
assert!(res.1.extract::<&str>().unwrap() == "world");
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.

Potentially just extract to String to eat the allocation but avoid the need for a feature gate on the test? Not really a perf concern here.

Also looks like this could be done with assert_eq!.

Suggested change
assert!(res.1.extract::<&str>().unwrap() == "world");
assert_eq!(res.1.extract::<String>().unwrap(), "world");

}

#[test]
// Python 3.10 limited API is needed for .extract::<&str>
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.

Same comment about maybe using String across all the extract points to remove the feature gates.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants