Skip to main content

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):

  1. @pytest.mark.swarm(max_workers=N) — per-test marker
  2. --swarm-workers=N — CLI option
  3. PYTEST_SWARM_WORKERS=N — environment variable
  4. os.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, monkeypatch and recwarn mutate 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_namespace and record_property are supported and stay on the parallel path.

    Run with --swarm-explain to 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)

Source distribution for pytest-swarm 0.1.7
File Size Uploaded Provenance
pytest_swarm-0.1.7.tar.gz 35.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-swarm 0.1.7
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page