Skip to content

Conversation

@ngoldbaum
Copy link
Contributor

@ngoldbaum ngoldbaum commented Jan 26, 2026

Fixes #5644

The first two commits fix some errors from #5746 that were missed in review.

The bulk of the changes are in the third commit, which updates the pymodule implementation to use the new PEP 793 APIs.

After that there are some commits for issues I encountered in CI.

Implementation notes

My original goal was to avoid defining PyModuleDef objects directly on the 3.15 and newer. It turns out the inittab tests need the legacy init hook, so I still need to unconditionally define a PyModuleDef. When we add support for a opaque PyObject ABI, we'll need to disable append_to_inittab on those builds.

I also had to work around a limitation of using both the PyModuleDef and slots, see this discourse post for more context. I'm asking in the discourse post whether it makes sense to relax the check in CPython that I'm working around by defining both SLOTS and SLOTS_MINIMAL in the pymodule macro implementation.

With the new APIs in 3.15, I'm then able to set up modules and submodules without any reference to a module object.

I replaced the const generic parameter on PyModuleSlots and PyModuleSlotsBuilder because we statically know how many slots PyO3 knows how to define, so it's not necessary for users of the API to keep track. See the logic defining the MAX_SLOTS variable and how it's used in the implementation.

#[cfg(not(Py_3_15))]
{
unsafe { *self.ffi_def.get() }.m_slots
}
Copy link
Contributor Author

@ngoldbaum ngoldbaum Jan 26, 2026

Choose a reason for hiding this comment

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

I thought it was easier to read if I repeat myself a bit in the Py_3_15 and not(Py_3_15) blocks. Let me know if anyone would prefer to see me collapse this a bit.

@ngoldbaum ngoldbaum added the CI-no-fail-fast If one job fails, allow the rest to keep testing label Jan 26, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 27, 2026

Merging this PR will not alter performance

✅ 99 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing ngoldbaum:moduledef-hide-details (9b62a4e) with main (12d57fe)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@ngoldbaum
Copy link
Contributor Author

Tests are passing now but I don't understand why the benchmark is failing or why the code coverage test is failing. Are the 3.15 test runners not uploading coverage reports, perhaps?

I'm going to add a changelog entry and mark this ready for review.

@ngoldbaum ngoldbaum marked this pull request as ready for review January 27, 2026 17:31
@ngoldbaum
Copy link
Contributor Author

@bschoenmaeckers do you happen to have any context about why my changes to type initialization are impacting the benchmark you added last week?

@bschoenmaeckers
Copy link
Member

@bschoenmaeckers do you happen to have any context about why my changes to type initialization are impacting the benchmark you added last week?

It has been jumping up and down since rust 1.93. I'm not sure what is going on. So it it safe to ignore for now

.with_doc(DOC)
.build();

static SLOTS_MINIMAL: PyModuleSlots = PyModuleSlotsBuilder::new()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Petr opened python/cpython#144340, which will hopefully allow us to avoid this dance in 3.15a6, due out the week after next. I'll try to make sure that gets merged.

Copy link
Member

Choose a reason for hiding this comment

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

I guess that would remove the need for SLOTS_MINIMAL in the macros as well (and makes my comment about PEP 820 irrelevant)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, see this discourse post and replies for some background on this. I was also unhappy with the ugliness of SLOTS_MINIMAL and Petr's PR lets me delete it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The CPython PR is in so we can wait to merge this PR until the next alpha is out and github actions picks it up. Should be tomorrow or the day after. I’ll also delete the SLOTS_MINIMAL hack.

Copy link
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, this looks great to me! Just a few small suggestions.

Comment on lines 566 to 567
});
result.extend(quote! {
Copy link
Member

Choose a reason for hiding this comment

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

I think you can do it in a single quote! invocation.

Suggested change
});
result.extend(quote! {

Comment on lines 535 to 542
// The full slots, used for the PyModExport initializaiton
static SLOTS: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new()
.with_mod_exec(__pyo3_module_exec)
.with_gil_used(#gil_used)
.with_name(__PYO3_NAME)
.with_doc(#doc)
.build();
Copy link
Member

Choose a reason for hiding this comment

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

This already makes me want slots reuse from PEP 820 😂

Comment on lines 319 to 327
assert!(
self.len < N,
self.len < MAX_SLOTS,
"N must be greater than the number of slots pushed"
);
Copy link
Member

Choose a reason for hiding this comment

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

the assert failed message probably wants to mention MAX_SLOTS instead of N. I also wonder if we can / should move this check or a variant of it to self.push()?

.with_doc(DOC)
.build();

static SLOTS_MINIMAL: PyModuleSlots = PyModuleSlotsBuilder::new()
Copy link
Member

Choose a reason for hiding this comment

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

I guess that would remove the need for SLOTS_MINIMAL in the macros as well (and makes my comment about PEP 820 irrelevant)?

Copy link
Member

Choose a reason for hiding this comment

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

With other test removed.

Suggested change
fn test_module_slots_builder_overflow() {

Comment on lines 500 to 507
PyModuleSlotsBuilder::new()
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.build();
Copy link
Member

Choose a reason for hiding this comment

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

To avoid needing to maintain in the future this probably works:

Suggested change
PyModuleSlotsBuilder::new()
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.with_mod_exec(module_exec)
.build();
let mut builder = PyModuleSlotsBuilder::new();
for _ in 0..MAX_SLOTS + 1 {
builder = builder.with_mod_exec(module_exec);
}
builder.build();

@ngoldbaum ngoldbaum force-pushed the moduledef-hide-details branch from 03d6398 to 3bd805a Compare February 6, 2026 21:32
@ngoldbaum
Copy link
Contributor Author

ngoldbaum commented Feb 6, 2026

I think I addressed all the comments. I also noticed that we weren't actually using the PyABIInfo slot in the pymodule macro and have fixed that in 3bd805a.

As a consequence, we will start getting nicer errors if someone tries to import an extension built using the wrong ABI. Here's what I see when I run a Python test using 3.15t then switch to 3.15 using with pyenv and re-run them without clearing the cargo build cache:

ImportError while importing test module '/Users/goldbaum/Documents/pyo3/pytests/tests/test_enums.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.pyenv/versions/3.15-dev/lib/python3.15/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_enums.py:2: in <module>
    from pyo3_pytests import enums
.nox/test/lib/python3.15/site-packages/pyo3_pytests/__init__.py:1: in <module>
    from .pyo3_pytests import *
E   ImportError: pyo3_pytests.pyo3_pytests: only compatible with free-threaded CPython

A bit nicer than a segfault!

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

Labels

CI-build-full CI-no-fail-fast If one job fails, allow the rest to keep testing free-threading

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement support for PEP 793

4 participants