Repository navigation
Thread safety and Pytest #13768
Description
(This is a high-level issue that proposes making pytest more thread safe; I'm definitely looking for feedback, discussion, and confirmation this is a good direction here.)
Background
This issue comes out of a brief conversation on the pytest discord:
[@ngoldbaum] out of curiosity, if there were funding for work on improving pytest’s thread safety, is that something that the project might be interested in? For the work we’ve been doing so far we’ve contracted David Woods to work on Cython, David Hewitt to work on PyO3, and Liam DeVoe to work on Hypothesis. Is there an opportunity for pytest as well?
[@The-Compiler] Personally I'm not too interested in it being thread-safe, but I'd absolutely support using our Opencollective funds to make it happen
[@RonnyPfannschmidt] my sentinent is the same - but i also want to note that i'd prefer thread safety in a manner that uses queues and non-shared state for fixtures & co (aka xdist-alike, just without separate node objects) - the key contention would be log/stdio capture state + managing pytest stores on nodes in a controlled manner
I don't presume to know the best direction for thread-safety in pytest, so I'm opening this issue to get some feedback on my understanding.
While this may be obvious to all involved, I want to at least give a brief motivation for thread-safety here. Python is working towards removing the global interpreter lock, in a python build version called free-threading. As free-threading progresses, it seems likely that more people will want to run multiple tests simultaneously in separate threads. In particular, it would be nice for free-threading development if pytest-xdist supported threading (in place of multiprocessing) as its worker primitive, so existing test suites can be run with pytest-xdist in threading mode to flush out concurrency bugs.
Initial proposal
Proposal
I would summarize all of pytest as two (very complex) steps:
- Test collection.
- Test execution.
with some calls to pluggy thrown in during both.
From talking with @Zac-HD, I understand there's some appetite for parallelizing collection in pytest itself using threads, perhaps as a consequence of folding pytest-xdist into pytest. This would require making (1) test collection thread safe. I think that parallelized test collection is "merely" a performance improvement, so I'm not proposing it in this issue, though it may be something to keep in mind.
Instead I want to focus on (2); making test execution thread-safe. Here is my understanding of the required work there:
- Make pluggy thread-safe, as hooks will be called from multiple threads.
- It's possible this work is either minimal or zero, if pluggy is already thread safe.
- Guarantee all (feasible) built-in fixtures are thread-safe.
- I don't (yet) know pytest internals enough to judge how difficult this will be.
- Some fixtures, like
monkeypatchandcapsys, don't seem possible to make thread-safe. Perhaps someone knows an arcanely cursed way to achieve this?
- Make pytest data collection during tests thread-safe.
- the tracking of the number of xfails, successes, failures, etc.
- Document the thread-safety guarantees, including what is and is not safe.
- Add an opt-in execution model to pytest-xdist which uses threads instead of processes.
- If we're done our job right up to this point, this should be conceptually simple, though it may require some involved changes to pytest-xdist to support this.
And finally;
- Test the heck out of this.
- I put a substantial amount of effort into testing thread-safety when migrating Hypothesis: Add threading CI job HypothesisWorks/hypothesis#4489. I would expect to do so again here, though I don't yet know the best way to do so.
Open questions
- If we do leave fixtures like
monkeypatchthread-unsafe, how should this interact with pytest-xdist's proposed threading mode? A few options: do nothing; print a warning; error; or skip the test. I don't like half of these!
cc: @ngoldbaum, @lysnikolaou
Refined Proposal
- separate
SetupStatefromSession- one
Sessioncollection-tree should be shared among all threads - each thread gets a separate
SetupStateand therefore a separate instance of each fixture
- one
- audit
Stashuses for thread-safety - audit
cacheproviderfor thread-safety - make
capsysthread-aware, with integrations for the per-threadSetupState
Activity
RonnyPfannschmidt commentedon Sep 30, 2025
there are multiple missunderstandings here
so my primary concerns would be detangling setup-state, enabling thread safety in node objects (in particular higher scope nodes)
then would be a follow-up with details like monkeypatch crossover (very hellish, most likely unsafe), log/stdio capture with python level standings & co
fixtures wouldn't need direct thread safety due to explicitly distinct thread safety
but we do need a plan for fixtures that mess with shared state (like modules/monkepatch/fdcapure)
we also need to investigate if subinterpreters will help
RonnyPfannschmidt commentedon Sep 30, 2025
also additional considerations are needed for fun bits like gui framework eventloops going broke on non-main threads and other curious ecosystem traps
Liam-DeVoe commentedon Sep 30, 2025
This broadly makes sense, but I wanted to clarify a few things:
pytest -n 8->pytest -n 8 --use-threads, pending naming) was a desirable outcome from all parties here. Is that not the case on pytest's end?Repeating back in my own words: "fixture instances should not be shared across threads; rather, each thread should maintain its own SetupState which pushes/pops/instantiates fixtures for that thread. Fixtures which inherently modify shared state (monkeypatch etc) will need special consideration." This makes sense to me, and I think thread-isolated fixtures is actually what @ngoldbaum recommended initially, and I misread.
RonnyPfannschmidt commentedon Sep 30, 2025
the core focus of pytest should be making it possible
we also have to consider async task models as possible
xdist has a focus on different processes, execnet and different nodes alltogether
we have had multiple experiments of having a simplefied xdist and i think its going to be something along the lines of splitting it up into scheduling concerns - thread concerns, async concerns - running in remote infra concerns
Zac-HD commentedon Sep 30, 2025
@RonnyPfannschmidt would you be able to write up your proposal for what Pytest should look like in the free-threaded future? Once we have agreement on that, we can work out how to get there incrementally.
For example, I think most people only use xdist to get multi-core local execution, and we could (imo should eventually) obsolete that with native multithreading in core pytest (with a pull-from-queue efficient scheduler). Then xdist can be multi-computer stuff only, though tbh I think this is usually a mistake and people should do independent runs then combine reports.
But the blocker to all of this seems to be having an agreed list of thread-safety-improving tasks which people can work on!
RonnyPfannschmidt commentedon Oct 1, 2025
we are probably able to do a rough proposal - but i suspect quite a bit of the details will change as we work towards it and learn
some key elements that come to mind off hand are:
bluetech commentedon Oct 3, 2025
Regarding monkeypatching etc, I'd say some tests are never going to be thread safe. We'd need a way to mark tests as not thread safe, or more likely the other way around.
RonnyPfannschmidt commentedon Oct 3, 2025
I think I'd want to be able to explicitly mark fixtures as safe, unsafe or hazard plus propagate implications to unmarked
Fixtures
Zac-HD commentedon Oct 3, 2025
I agree we'll need that eventually, but I'd start by working on the minimal core, e.g.
pytest --capture=no -p no:monkeypatchwithout use of fixtures. Once we get multiple pytest sessions and node-stores to be threadsafe and tested as such, we can incrementally add all the other stuff users need, and finally the (very, perhaps fundably) nice-to-haves like parallel collection.I expect that we'll also see some improvements elsewhere in the ecosystem which make this easier, e.g. here's a proposal for
contextlib.redirect_stdout()to become thread-aware which we'd be able to use (eventually).RonnyPfannschmidt commentedon Oct 4, 2025
Well never be able to use those contextmanagers unless python fundamentally changes io redirection
For thread/logging safety Well have to essentially have thread context aware replacements that include pytest integration
bluetech commentedon Oct 4, 2025
I was also worried about warnings, but it seems that
catch_warningsis handled in Python 3.14, so pytest's warnings plugin should work at least.Do I understand correctly that the idea here is that collection is done in a non-parallel phase on the main thread, and the same collection tree is shared among multiple parallel runners? As opposed to xdist which runs collection per worker.
ngoldbaum commentedon Oct 4, 2025
And in-practice I haven't seen any thread safety issues due to uses of
pytest.warnsin 3.14, so any remaining thread safety issues in pytest itself should be hard to trigger, if there are any.Liam-DeVoe commentedon Oct 5, 2025
Thanks for the discussion here. Here's a more refined todolist:
SetupStatefromSessionSessioncollection-tree should be shared among all threadsSetupStateand therefore a separate instance of each fixtureStashuses for thread-safeycapsysthread-aware, with integrations for the per-threadSetupStateThe working goal in my mind is to enable pytest support for the following implementation of a parallel
pytest_runtestloop:which could be implemented by e.g. pytest-run-parallel or a simplified pytest-xdist as they see fit. (I thought implementing this parallelism in a plugin might require additional pytest hooks, but I think
pytest_runtestloopcovers it.)I don't think Quansight or @ngoldbaum necessarily sees it as a requirement that pytest itself ship such a parallel execution model, merely that pytest makes it possible to implement one, though feel free to correct me. As a result, I don't think we yet have to address concerns like "monkeypatch is inherently thread-unsafe", because that can be a caveat/requirement of writing such a parallel
pytest_runtestloop. But if we did, then an extension is:monkeypatch[I also forgot to mention in the original post, but #13679 would be wonderful to have merged for pytest-run-parallel!]
This is my understanding yes, with a possible eventual goal to make collection itself parallel as an extension to this issue.
RonnyPfannschmidt commentedon Oct 5, 2025
note that this very naive testloop implementation wrecks havoc wrt sensible reorder to minimize setup/teardown
one would want shared setup/teardowns in "runs" that minimize the need to setup/teardown specific fixtures
aka try to make high scope fixtures sticky to specific treads so we remove the need for setup/teardown cycles
RonnyPfannschmidt commentedon Oct 5, 2025
oh - and one extra fun consideration - specific event loops will break outside of the main thread (i recall at least QT and gtk)
ngoldbaum commentedon Oct 13, 2025
We ran into this working on pytest-run-parallel as well. See this comment in the CPython repo for an outline of a way to do this.
EDIT: And I see Zac posted about this later in the thread...
ngoldbaum commentedon Oct 13, 2025
Liam's summary seems like an excellent plan and would substantially help ecosystem efforts to improve multithreaded test coverage. Currently there's lots of manual patching that's needed to work around pytest thread safety issues that Liam's plan would fix. If each thread had a local fixture and if setup and teardown happened in a thread-safe manner, @bwhitt7's recent work on using pytest-run-parallel in the NumPy CI would have been a lot easier, just as a concrete example. Pairing pytest-run-parallel with compiler sanitizers is already pointing out real bugs.
Thanks all for your active participation and ideas on this thread, I'm really excited about where this can go.
bluetech commentedon Nov 21, 2025
To limit the scope of this, I think we should limit the usage of
--threads(just gonna assume that will be the flag 😀 ) to the freethreaded build. The reasons being:Without free threading the feature is probably not so useful (not useless, but less useful for the main purpose of speeding up the test suite).
It allows us to focus on a new Python baseline (e.g. no Python 3.10 headaches).
The free threaded build enables two very relevant flags by default,
sys.flags.thread_inherit_contextandsys.flags.context_aware_warnings. See here (python docs are currently 503 so linking to github instead).With
sys.flags.thread_inherit_contextI think we can make a usablecapsysreplacement usingContextVar. Usingthreading.localalone wouldn't be good since it will fail to capture in tests which themselves launch threads.Using
ContextVaris problematic as well, tests which launch threads or tasks without inheriting the context will then fail to capture. For threads with thesys.flags.thread_inherit_contextI'd be less worried. Async tests probably do this more, although I think evenasyncio.runinherits context from the running thread, so maybe it's fine.Regarding fixtures @RonnyPfannschmidt wrote:
@Liam-DeVoe wrote:
We should decide what we want to do here.
Liam's idea to have entirely separate SetupState's is the simplest way, similar to how xdist works. Is it workable in practice?
What I think Ronny is aiming at is for threads to share higher-scope fixtures. This will bring a significant advantage over xdist, but will no longer be a "shared nothing" architecture, so a significant up in the complexity. Also due to setup/teardown expectations, it will have heavy synchronization overhead and basically serialize the threads in some cases, unless the scheduler (what assigns tests to threads) is smart enough to avoid that. (And don't forget higher-scoped fixtures can be parametrized, meaning setup/torndown multiple times.)
@RonnyPfannschmidt did I understand correctly?
Surely a simple "fixture is higher scope => fixture is sticky to one thread" wouldn't work, since a single session-scoped fixture will force all tests to a single thread.
RonnyPfannschmidt commentedon Nov 21, 2025
Stickyness is about parameterized fixtures
We should cluster based on sheduling setup/teardown
No actual instances should share for now
ngoldbaum commentedon Nov 21, 2025
Your reasons make sense, but since it's possible to opt into
threads_inherit_contextandcontext_aware_warningson the GIL-enabled build and opt out on the free-threaded build, maybe instead just make it conditional on the value of the startup flags?I've found it very useful in-practice to check whether a bug reproduces on the free-threaded build and the GIL-enabled build or just the free-threaded build. Often also it helps to set the thread switch interval to a very small value on the order of a microsecond to trigger multithreaded bugs more often on the GIL-enabled build.
yup!
Details
RonnyPfannschmidt commentedon Nov 21, 2025
a threaded mode can also make sense on non-free-threading
any acceptance test thats network bound on something like selenium/playwright or rest services can gain wins not dissimilar from xdist (with the caveat that the only win is intersparsed concurrency
but its also abundantly clear that an cpu-bound execution will not take a win home there
Liam-DeVoe commentedon Jan 21, 2026
An update - here's my current progress on thread safety: https://github.com/pytest-dev/pytest/compare/main...Liam-DeVoe:pytest:threading-work?expand=1.
This branch requires each thread that runs
runtestprotocolto follow apytest-thread-{n}naming convention, which pytest uses to track that thread as a "pytest thread". This branch usesContextVarand assumesthread_inherit_context=1is set, so that threads spawned by tests inherit knowledge of the current "pytest thread" and integrate accordingly.In this branch, each pytest thread gets its own copy of each fixture's value. Fixtures are effectively thread-local. (*they are really pytest-thread-local, as threads spawned inside tests still use that pytest thread's fixture value).
Note that I think this is a somewhat less structural refactoring than the separation of
SetupStateandSessionwhich has been suggested in this thread. I'm curious to hear thoughts on this, and whether this should go further in refactoring pytest internals.I'm pretty excited to report that this branch lets me run most of the Hypothesis test suite in parallel, when combined with the appropriate
conftest.py:Details
Stumbling blocks are that it currently requires
--capture no -p no:cacheprovider. The former will definitely require some work to fix, whereas I'm merely somewhat skeptical of the latter's thread-safety, and am keeping it out of an abundance of caution for now.RonnyPfannschmidt commentedon Jan 21, 2026
Please open a draft pr for easy review
There's a number of bugfix prs surrounding fixtures si theres conflict potential
As for capture i believe we need threadmapped fd capture that has context aware replacements for the streams (bur that idea is not yet batle trsted)
Refactor update_manifest_for_old_job cli tests
Refactor update_manifest_for_old_job cli tests