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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_swarm-0.1.6.tar.gz (32.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_swarm-0.1.6-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file pytest_swarm-0.1.6.tar.gz.

File metadata

  • Download URL: pytest_swarm-0.1.6.tar.gz
  • Upload date:
  • Size: 32.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pytest_swarm-0.1.6.tar.gz
Algorithm Hash digest
SHA256 91242aef480c6b612ab8e01843f28c87052c0f8f11f9aa1e301a010577a21eff
MD5 e354f2d43ec1c52314084c967c20825c
BLAKE2b-256 f03a6bd199107efc35a86aed25b06d957ae58e38d7b243b06068401cac86d240

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_swarm-0.1.6.tar.gz:

Publisher: release.yaml on dpadninja/pytest-swarm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_swarm-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: pytest_swarm-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 20.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pytest_swarm-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 a3f3b7103d5d4f13fd51c7afd32ec0d752946047a4939e9a2251772f757333ea
MD5 01327dec2bad029eec7efaf61112c15d
BLAKE2b-256 47f21cf41e5d59249dd8cb66b82eada5c0f2f50d1b24679e917fe68bba361d09

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_swarm-0.1.6-py3-none-any.whl:

Publisher: release.yaml on dpadninja/pytest-swarm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.6 This release

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

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