Skip to content

Add Python 3.13 and 3.14 support#28

Open
godlygeek wants to merge 4 commits into
mainfrom
add-python-3.13-3.14-support
Open

Add Python 3.13 and 3.14 support#28
godlygeek wants to merge 4 commits into
mainfrom
add-python-3.13-3.14-support

Conversation

@godlygeek

Copy link
Copy Markdown
Contributor

Closes #15

This resurrects #16, which slipped through the cracks and got closed without ever being merged. Sorry about that, @zeel2104!

I've rebased it on main and dropped a few diff hunks that we don't want. Most importantly, I want to keep the python-version: ${{ matrix.python_version }}-dev (we don't need the -dev suffix right now, but we'll need it again soon when we add 3.15 support). Other than that, I reverted some changes to things that were already addressed by other PRs.

@godlygeek godlygeek self-assigned this Jun 7, 2026
@godlygeek godlygeek force-pushed the add-python-3.13-3.14-support branch 3 times, most recently from 8f31cb7 to b8766d8 Compare June 7, 2026 07:22
@godlygeek

Copy link
Copy Markdown
Contributor Author

And, unfortunately, it turns out that there were a bunch of other changes needed on top of that, to correctly cope with Python 3.14 changing the default multiprocessing start method on Linux from "fork" to "forkserver". I think I've got them all ironed out now, though.

@godlygeek godlygeek requested review from pablogsal and sarahmonod June 7, 2026 07:28
godlygeek added 2 commits June 7, 2026 03:31
Since Python 3.14 'forkserver' is now the default multiprocessing start
method instead of 'fork', and with this spawn method we need to pass the
`multiprocessing.Event` that we're sharing between the pytest process
and the monitoring process along explicitly, rather than counting on
a `fork()` to share the same object between the two processes.
Python 3.14 defaults to the "forkserver" multiprocessing start method,
instead of the previous "fork" default. This new default causes all
sorts of problems for us.

The hardest to fix one, which made me throw in the towel on trying to
get things working with "forkserver", is that pytest's `pytester`
fixture defaults to running the new pytest session in the current
process, rather than from a subprocess, and we're using `capfd` to
capture the stderr. With the "fork" method and the "spawn" method, the
new process inherits whatever the parent process's stderr (fd 2) is
pointed to at the time when the new process is started, but with
"forkserver" the server gets created once and then reused, and all
future children have their stderr pointed wherever the first child
process's stderr was pointed to.

There's other issues too. We leak semaphores because the monitoring
process winds up creating an unnecessary `Event` and `Queue` that it
ignores in favor of the ones passed to its entry point. That could be
fixed by creating those lazily instead of at import time, but that leads
to pickling failures because the `multiprocessing.connection` module
gets created lazily, and `pytester` unloads modules that were loaded by
the tests, which leads to two different copies of that module getting
loaded, with the old module still referenced by some things.

We could work around all of this in our test suite, but using our plugin
along with the `pytester` fixture is a reasonable use case that end
users might want, if they're trying to use `pytest-pystack` to test
their own pytest plugin.

The most reasonable solution is for us to detect when "forkserver" would
be used, and to use "spawn" instead in those cases.
@godlygeek godlygeek force-pushed the add-python-3.13-3.14-support branch from b8766d8 to ef2ee8e Compare June 7, 2026 07:32
@godlygeek

Copy link
Copy Markdown
Contributor Author

@pablogsal I'd appreciate your eyes here. This turned out to be shockingly tricky, and I'd appreciate a double check to make sure there's nothing else I'm missing.

Rather than trying to support both "fork" and "spawn", force "spawn" to
be used no matter what.

Split the monitor process's entry point out into a separate module to
avoid having a global multiprocessing `Queue` object get created when it
is reimported in the spawned process, as otherwise Python 3.8 and 3.9
report that some semaphores are leaked by the child process.
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.

Add support for Python 3.13 and 3.14

3 participants