Parallel test execution using threads — true parallelism on free-threaded Python, concurrent I/O on standard builds
Project description
pytest-threadpool
Status: Beta · Parallel test execution using threads.
Runs test bodies, function-scoped fixture setup, and function-scoped fixture teardown concurrently in a thread pool while keeping shared fixtures (module/class/session scope) sequential.
Works on any Python 3.13+. Free-threaded builds (3.13t, 3.14t, 3.15t) get true parallelism for CPU-bound tests. Standard builds still benefit from parallel execution of I/O-bound tests (network, database, file operations).
Installation
pip install pytest-threadpool
Quick start
pytest --threadpool auto
Mark tests for parallel execution:
from pytest_threadpool import parallelizable, not_parallelizable
import pytest
@parallelizable("children") # all nested tests run in parallel
class TestMyFeature:
def test_a(self): ...
def test_b(self): ...
@parallelizable("parameters") # parametrized variants run in parallel
@pytest.mark.parametrize("x", [1, 2, 3])
def test_with_params(x): ...
@parallelizable("all") # children + parameters combined
class TestEverything:
@pytest.mark.parametrize("n", [1, 2])
def test_param(self, n): ...
def test_plain(self): ...
@not_parallelizable # opt out of inherited parallelism
def test_must_be_sequential(): ...
Scopes
| Scope | Effect |
|---|---|
children |
All nested tests run concurrently (children, grandchildren, etc.) |
parameters |
Parametrized variants of the same test run concurrently |
all |
Combines children + parameters |
Fixture handling
Function-scoped fixtures are cloned per-item and set up in parallel alongside test calls. Each worker gets independent fixture instances — no shared mutable state between concurrent fixture setups.
Shared fixtures (module, class, and session scope) are resolved once sequentially before workers launch and served from cache to all items.
| Scope | Behavior |
|---|---|
function |
Cloned per-item, setup and teardown in parallel workers |
class |
Resolved once, cached, shared across items |
module |
Resolved once, cached, shared across items |
session |
Resolved once, cached, shared across items |
Shared fixture teardown runs sequentially after the parallel group completes.
Marker levels
Markers can be applied at function, class, module (pytestmark), or
package (__init__.py pytestmark) level. Priority (most specific wins):
not_parallelizable > own marker > class > module > package
Shared state between tests
Unlike pytest-xdist, which uses subprocesses and requires all test data to
be pickleable, pytest-threadpool runs tests in threads within a single
process. This means tests can share common non-pickleable, thread-safe
objects — both within a parallel group and across sequential groups:
import threading
from pytest_threadpool import parallelizable
class SharedState:
lock = threading.Lock() # not pickleable
event = threading.Event() # not pickleable
results = {}
@parallelizable("children")
class TestGroupA:
def test_a1(self):
with SharedState.lock:
SharedState.results["a1"] = True
def test_a2(self):
with SharedState.lock:
SharedState.results["a2"] = True
@parallelizable("children")
class TestGroupB:
def test_b1(self):
SharedState.event.set()
with SharedState.lock:
SharedState.results["b1"] = True
def test_b2(self):
assert SharedState.event.wait(timeout=10)
with SharedState.lock:
SharedState.results["b2"] = True
Objects like threading.Lock, threading.Event, logging.Logger, database
connections, and other non-pickleable resources can live as class attributes
and be accessed freely from any test — parallel or sequential — without
serialization overhead or workarounds.
Usage
# Auto-detect thread count
pytest --threadpool auto
# Fixed thread count
pytest --threadpool 8
# Normal sequential run (no flag)
pytest
To enable --threadpool by default, add it to your config:
pyproject.toml (pytest 9.0+):
[tool.pytest]
addopts = ["--threadpool", "auto"]
pytest.ini:
[pytest]
addopts = --threadpool auto
Configuration
Command-line options
| Option | Values | Default | Description |
|---|---|---|---|
--threadpool |
N or auto |
(off) | Enable parallel execution with N threads. auto uses os.cpu_count(). |
--threadpool-output |
classic, live |
classic |
Output mode. live opens an interactive terminal viewer with scroll, tree navigation, and real-time updates. |
INI settings
Set in pyproject.toml, pytest.ini, or setup.cfg:
| Setting | Type | Default | Description |
|---|---|---|---|
threadpool_tree_width |
int | 0 |
Width (columns) of the live-view tree pane. 0 = auto (1/4 of terminal width). |
pyproject.toml:
[tool.pytest]
threadpool_tree_width = "35"
# or under ini_options
[tool.pytest.ini_options]
threadpool_tree_width = "35"
pytest.ini:
[pytest]
threadpool_tree_width = "35"
Live-view keybindings
Available when --threadpool-output live is active:
| Key | Action |
|---|---|
↑ ↓ |
Scroll content / move tree cursor |
PgUp PgDn |
Page scroll |
Home End |
Jump to top / bottom |
Tab |
Toggle tree panel (split-pane) |
Enter |
Show test/group output in content pane |
← → |
Collapse/expand tree node |
Ctrl+← Ctrl+→ |
Switch keyboard focus between tree and content panes |
Ctrl+P |
Toggle show/hide passed tests in tree |
Ctrl+X |
Toggle show/hide failed tests in tree |
/ |
Search within content pane (when focused) |
n / N |
Jump to next / previous search match |
Ctrl+W |
Save current pane content to .log file |
| Mouse scroll | Scrolls whichever pane the cursor is over |
Escape |
Clear search / close tree |
Ctrl+C |
Exit |
The tree panel shows the full test hierarchy (session > packages > modules > classes > tests)
with live outcome markers (✓ passed, ✗ failed, E error, s skipped, x xfail, X xpass).
Groups show aggregated status. Type in the tree panel to fuzzy-filter tests (fzf-style).
Tested versions
| Component | Versions |
|---|---|
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
| pytest | 9.0.2 |
Note: On standard (GIL-enabled) builds, the GIL limits parallel speedup for CPU-bound tests. I/O-bound tests still run concurrently.
Examples
The examples/ directory contains runnable usage patterns:
- DI container — dependency injection with Singleton, ThreadLocal, ContextLocal, and Factory scopes
- Event bus — shared in-memory test double with concurrent producers and aggregate verification
- Parallel logging — shared thread-safe log collector (caplog alternative)
- Shared state — barriers, atomic counters, and cross-group coordination
- User pool — custom thread pool with LIFO queue recycling
The tests/integration_tests/cases/ and
tests/integration_tests/ directories are also
worth browsing for real-world grouping, fixture, and reporting scenarios.
Known limitations
- Private pytest API usage — The plugin relies on internal
_pytestAPIs (fixture finalizers, setup state, terminal writer) that have no public equivalents. These may break across pytest releases without warning. - No plugin compatibility guarantees — Interactions with other pytest
plugins (e.g.
pytest-xdist,pytest-timeout,pytest-randomly) are untested and may conflict. - No
capsys/capfdin parallel — These fixtures are not thread-safe.capsys/capfdfail with "cannot use capsys and capsys at the same time" when requested by parallel tests. Alternatives:print()— Worker output is buffered by thread-local stream proxies and reported alongside each test's result. In default capture mode, output appears in "Captured stdout call" sections on failure. With-vs(--capture=no -v), captured output is emitted after the PASSED/FAILED line for each test. TTY mode (default without-s) suppresses print output; use-vsto see it. A TTY-friendly output viewer with switchable per-thread/per-test frames is planned (see ROADMAP).caplog— Thecaplogfixture works natively in parallel tests.caplog.records,caplog.text,caplog.record_tuples,caplog.messages,caplog.clear(),caplog.at_level(), andcaplog.set_level()all behave the same as in sequential pytest. Each worker gets its ownLogCaptureHandlerwith per-thread record isolation — parallel tests don't leak records into each other's caplog. Failed tests show "Captured log call" sections as expected.logging.Logger— Standardloggingcalls (logger.info(),logger.warning(), etc.) work natively in parallel tests. A thread-local log handler captures records per-worker and attaches them to "Captured log call" report sections on failure, matching sequential pytest behavior. Log output does not appear in stdout for passing tests unless--log-cli-levelis set or aStreamHandlerexplicitly targetssys.stdoutorsys.stderr.--log-levelcontrols which records are captured. ExistingStreamHandlerinstances are automatically redirected through the per-thread proxy so their output is grouped per-test instead of interleaving globally.- IDE and CI runners (PyCharm, TeamCity, VS Code) — Each test's
function-scoped setup, call, and teardown output appears in its own
report. Shared fixture output (session/package/module/class setup
and teardown) is emitted at the suite level, not inside individual
tests. Parametrized tests are reported in collection order so
runners group them correctly. Works via the
teamcity-messagesplugin (--teamcityflag); PyCharm and TeamCity CI use the same protocol. - Custom output hook — Implement
pytest_threadpool_reportin a conftest or plugin to customize how captured worker output is handled. ReturnTrueto suppress the default output handling.
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_threadpool-0.4.0.tar.gz.
File metadata
- Download URL: pytest_threadpool-0.4.0.tar.gz
- Upload date:
- Size: 140.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f010b58c7605bf00a0fd02dfbd720147e65945e2a128cfbaa31d7dce6cb9991a
|
|
| MD5 |
f504964f108dbdfe6708ac39e90863bf
|
|
| BLAKE2b-256 |
0a7edd6d818d0446758f526a2f2941952055af5df661104e9c465318a03897ad
|
Provenance
The following attestation bundles were made for pytest_threadpool-0.4.0.tar.gz:
Publisher:
publish.yml on pytest-threadpool/pytest-threadpool
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_threadpool-0.4.0.tar.gz -
Subject digest:
f010b58c7605bf00a0fd02dfbd720147e65945e2a128cfbaa31d7dce6cb9991a - Sigstore transparency entry: 1129139703
- Sigstore integration time:
-
Permalink:
pytest-threadpool/pytest-threadpool@a7d58bdcfe8d9ed0774cb1da12755ecb7243a1f1 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pytest-threadpool
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a7d58bdcfe8d9ed0774cb1da12755ecb7243a1f1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytest_threadpool-0.4.0-py3-none-any.whl.
File metadata
- Download URL: pytest_threadpool-0.4.0-py3-none-any.whl
- Upload date:
- Size: 64.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b97ab783f1d67843bcb4846fa5c3a57e70245a1b26daaf4d2cb9cdd72a39f7a
|
|
| MD5 |
476a6dd9eea2186d8aebf640c1c55149
|
|
| BLAKE2b-256 |
81a6078b6f8a029c7e11841e479a1d6e79891b169ff293bc9b255c16b9aab9ba
|
Provenance
The following attestation bundles were made for pytest_threadpool-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on pytest-threadpool/pytest-threadpool
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_threadpool-0.4.0-py3-none-any.whl -
Subject digest:
6b97ab783f1d67843bcb4846fa5c3a57e70245a1b26daaf4d2cb9cdd72a39f7a - Sigstore transparency entry: 1129139746
- Sigstore integration time:
-
Permalink:
pytest-threadpool/pytest-threadpool@a7d58bdcfe8d9ed0774cb1da12755ecb7243a1f1 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pytest-threadpool
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a7d58bdcfe8d9ed0774cb1da12755ecb7243a1f1 -
Trigger Event:
push
-
Statement type: