pytest-swarm
A pytest plugin that runs parametrized test variants in parallel threads — with correct fixture lifecycle.
What problem does it solve?
When a test is parametrized over many values (hosts, configs, datasets), pytest runs every variant sequentially by default. If each variant takes 2 s and you have 50 variants, the suite takes 100 s even though the variants are completely independent.
pytest-swarm solves exactly this one problem: it runs the variants of a single
@pytest.mark.swarm-decorated test in parallel threads, cutting wall-clock time
to roughly max(variant_times) instead of sum(variant_times).
Not a replacement for pytest-xdist or pytest-parallel
pytest-xdist distributes entire tests across processes or machines.
pytest-parallel also parallelizes at the test level.
pytest-swarm operates one level lower: it parallelizes the variants of a
single parametrized test while everything else — fixture lifecycle, test
ordering, reporting — stays exactly as in a normal sequential run.
Thread safety is your responsibility
Worker threads share the same process. Any shared mutable state accessed from parallel test bodies — global variables, module-level caches, resources held in broad-scope fixtures — must be protected by locks or other synchronization primitives. The plugin guarantees that broad-scope fixtures are created once in the main thread, but it does not add any locking around how you use them inside the test body.
Installation
pip install pytest-swarm
Quick start
import pytest
import time
@pytest.fixture(scope="module")
def config():
return load_config() # created once, shared by all variants
@pytest.fixture
def client(config):
return Client(config) # created in parallel, one per variant
@pytest.mark.swarm(max_workers=4)
@pytest.mark.parametrize("host", ["h1", "h2", "h3", "h4"])
def test_ping(host, client):
time.sleep(1) # 4 variants run simultaneously → ~1 s total, not ~4 s
Without max_workers — uses CPU count.
Controlling worker count
Priority chain (highest to lowest):
@pytest.mark.swarm(max_workers=N)— per-test marker--swarm-workers=N— CLI optionPYTEST_SWARM_WORKERS=N— environment variableos.cpu_count()— default
CLI
pytest --swarm-workers=4
Environment variable
PYTEST_SWARM_WORKERS=4 pytest
Useful for CI pipelines where you want a consistent cap without modifying test code. The CLI option takes priority over the env variable.
Per-test override
@pytest.mark.swarm(max_workers=16) # overrides both CLI and env var
@pytest.mark.parametrize("host", hosts)
def test_connect(host):
...
Fixture scope behavior
The plugin respects pytest fixture scopes. Behavior depends on whether fixtures are function-scoped or broader.
Function-scope — full parallel lifecycle
Each parametrized variant runs its entire lifecycle (setup → call → teardown) in its own thread. Fixture setup runs in parallel — useful when the fixture itself is expensive (e.g. establishing a connection).
@pytest.fixture
def connection(request):
conn = connect(request.param) # runs in parallel across threads
yield conn
conn.close()
@pytest.mark.swarm(max_workers=6)
@pytest.mark.parametrize("connection", hosts, indirect=True)
def test_command(connection):
assert connection.run("uptime")
Works with all function-scope parametrization forms:
| Form | Works |
|---|---|
@pytest.mark.parametrize("n", [...]) |
✓ |
@pytest.fixture(params=[...]) |
✓ |
@pytest.mark.parametrize("fix", [...], indirect=True) |
✓ |
pytest_generate_tests hook |
✓ |
| fixture depending on another fixture | ✓ |
pytestmark = pytest.mark.usefixtures(...) |
✓ |
Class / module / package / session scope — shared instance, parallel calls
Broad-scope fixtures are created once in the main thread and shared between all parallel test variants. Only the test body runs in parallel.
@pytest.fixture(scope="module")
def pool():
return ConnectionPool(size=10) # created once; must be thread-safe
@pytest.mark.swarm(max_workers=10)
@pytest.mark.parametrize("cmd", commands)
def test_run(cmd, pool):
pool.run(cmd) # parallel — pool must handle concurrent access
SETUP and TEARDOWN happen exactly as many times as they would in a normal sequential run — once per fixture scope boundary.
Mixed scopes
When a test uses both function-scope and broad-scope fixtures, the broad-scope fixture is created once (serial), and each variant gets its own function-scope instance (parallel setup).
Autouse fixtures from plugins
Autouse session/module-scope fixtures from third-party plugins (e.g.
_session_faker from pytest-Faker) are present in item.fixturenames but are
correctly ignored when deciding the execution path. They don't force a sequential
fallback.
Non-parallel tests
Tests without @pytest.mark.swarm run normally — the plugin does not affect
them. Parallel and sequential tests can coexist freely in the same session.
def test_sequential(): # unaffected
...
@pytest.mark.swarm(max_workers=4)
@pytest.mark.parametrize("n", range(4))
def test_parallel(n): # parallel
...
Plugin compatibility
Most pytest hooks work correctly for swarm tests:
| Hook | Works |
|---|---|
pytest_runtest_logstart / logfinish / logreport |
✓ |
pytest_runtest_makereport (hookwrapper) |
✓ |
pytest_runtest_protocol (hookwrapper) |
✓ |
pytest_runtest_protocol (regular @pytest.hookimpl) |
✗ see below |
pytest.mark.xfail / pytest.mark.skip |
✓ |
pytest_runtest_protocol — hookwrapper only
pytest_runtest_protocol is a firstresult hook: the first implementation that
returns a non-None value wins and the rest of the chain is skipped. The plugin
implements this hook with tryfirst=True and returns True to prevent pytest's
default runner from executing the test again via SetupState.
Because of this, a regular implementation in a conftest —
# conftest.py
@pytest.hookimpl # does NOT fire for swarm tests
def pytest_runtest_protocol(item, nextitem):
...
— is never reached: the plugin's tryfirst implementation runs first and stops
the chain. This does not affect hookwrappers, which always wrap the full chain
regardless of firstresult:
@pytest.hookimpl(hookwrapper=True) # works correctly
def pytest_runtest_protocol(item, nextitem):
...
yield
In practice this is rarely a concern: the overwhelming majority of plugins that
observe pytest_runtest_protocol (pytest-xdist, pytest-rerunfailures, etc.) use
hookwrapper=True.
Limitations
-
Broad-scope fixture setup is serial. Only the test body is parallelized when class/module/package/session fixtures are involved. Move expensive operations into the test body or use a connection pool pattern to work around this.
-
Thread safety is the test's responsibility. Shared mutable state accessed from parallel test bodies must be protected by locks or other synchronization.
-
Some built-in pytest fixtures cannot be parallelized.
capsys,capfd,caplog,monkeypatchandrecwarnmutate process-global state — captured file descriptors, logging handlers,os.environ— which no amount of per-thread instancing makes safe. A group that needs one of them runs sequentially through pytest's ordinary protocol, and says so.tmp_path,tmp_path_factory,pytestconfig,cache,doctest_namespaceandrecord_propertyare supported and stay on the parallel path.Run with
--swarm-explainto see how each group was executed and why:$ pytest --swarm-explain ================================ swarm plan ================================= parallel 8 item(s) 8 worker(s) tests/test_api.py::test_fetch sequential 3 item(s) no threads tests/test_io.py::test_capture built-in fixture 'capsys' is not supported in worker threads
How it works
function-scope group time →
thread 1 [ setup ][ test body ][ teardown ]
thread 2 [ setup ][ test body ][ teardown ] ← all run simultaneously
thread 3 [ setup ][ test body ][ teardown ]
broad-scope group time →
main [ setup ] [ teardown ]
thread 1 [ test body ]
thread 2 [ test body ] ← all run simultaneously
thread 3 [ test body ]
Fixture functions are called directly inside threads (bypassing pytest's
SetupState), so function-scope setup runs truly in parallel. Broad-scope
fixtures go through normal pytest setup in the main thread to preserve shared
instance semantics.
License
MIT — see LICENSE.
Metadata
Release files for pytest-swarm 0.1.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pytest_swarm-0.1.7.tar.gz | 35.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_swarm-0.1.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.5 kB
Release files / pytest_swarm-0.1.7.tar.gz
| Download URL | pytest_swarm-0.1.7.tar.gz |
|---|---|
| Size | 35.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c319386cc543813a2d41a5661de06d535d4de3e93c040136b242edc0ea36d338
|
|
BLAKE2b-256 checksum How to use checksums |
59dc3fa6c857e484c3f81f98353314302174075157803243b7a3cc29c78a4e71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / pytest_swarm-0.1.7-py3-none-any.whl
| Download URL | pytest_swarm-0.1.7-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
044d543f6cbba15e19ae038d847074349954dc17a420483df55acad98dfb9bc1
|
|
BLAKE2b-256 checksum How to use checksums |
b4d504a7b690e73d587b3ea5cd03e7d4a0bd045b342d2bce645369595d96a67c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logSecurity for pytest-swarm 0.1.7
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pytest_swarm-0.1.7.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.