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

  • Built-in pytest fixtures (tmp_path, capfd, monkeypatch, …) in the fixture dependency chain may not work in the parallel-setup path. The plugin falls back to serial setup automatically when it detects them.

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.4.tar.gz (26.3 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.4-py3-none-any.whl (16.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pytest_swarm-0.1.4.tar.gz
  • Upload date:
  • Size: 26.3 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.4.tar.gz
Algorithm Hash digest
SHA256 0f64964796abc207010d90f3b46f83ae9877aab7f158c4677c353ff5b713f3c8
MD5 789425827bcda0c399c1bab7cdca2d42
BLAKE2b-256 b4ee1b360944c5db10c624c0c099cd43e89eb101df088add05b11e87c42f6e11

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_swarm-0.1.4.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.4-py3-none-any.whl.

File metadata

  • Download URL: pytest_swarm-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 16.4 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.4-py3-none-any.whl
Algorithm Hash digest
SHA256 baf8deb43cc4bf0e4b0fd2006c41d2e3cdf5ba0557065377806aefc9a696f25a
MD5 6ce21052d756eafce7471acfe601cfb0
BLAKE2b-256 f700184e607565585eb85f114a48de64e7a7d85e4dcf8f59653043c6185cd699

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_swarm-0.1.4-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

0.1.6

2 files

0.1.5

2 files

This release

0.1.4 This release

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