How to run tests in parallel with pytest-xdist
A large test suite that runs sequentially can take minutes. pytest-xdist distributes tests across multiple worker processes, which can lead to considerable speed ups depending on the test suite and hardware.
Installation
Add pytest-xdist as a dev dependency using uv:
uv add --dev pytest-xdistRunning tests in parallel
Pass -n auto to let pytest-xdist detect the number of available CPU cores and spawn that many workers:
uv run pytest -n autoTo specify an exact number of workers:
uv run pytest -n 4Each worker runs in its own process with its own Python interpreter. Tests are distributed to workers as they become available. The output is collected and displayed as a single report.
Making it the default
Add -n auto to your pyproject.toml so parallel execution happens without remembering the flag:
[tool.pytest.ini_options]
addopts = "-n auto"To temporarily override this and run tests sequentially (useful for debugging):
uv run pytest -n 0When tests are not safe to parallelize
Parallel execution assumes tests are independent. Tests that share mutable state will produce intermittent failures when run concurrently. Common sources of trouble:
Shared files or databases. Two workers writing to the same SQLite file or temp directory will collide. Use pytest’s tmp_path fixture to give each test its own directory, or use session-scoped fixtures with worker-aware setup.
Shared external resources. Because each worker is a separate process, in-memory state like module-level variables and singletons is already isolated. The real problems come from shared external resources: databases, files on disk, network ports, or external services that multiple workers access simultaneously.
Port binding. Tests that start a server on a fixed port will fail when two workers try to bind the same port. Use port 0 to let the OS assign a free port.
Worker-scoped fixtures
pytest-xdist assigns each worker an ID (gw0, gw1, etc.). Fixtures that need to be unique per worker can use worker_id:
@pytest.fixture(scope="session")
def db_name(worker_id):
return f"test_db_{worker_id}"For session-scoped fixtures that should run only once across all workers (not once per worker), use tmp_path_factory with a file lock. The first worker to acquire the lock performs setup; others wait and then read the result:
import pytest
from filelock import FileLock
@pytest.fixture(scope="session")
def shared_data(tmp_path_factory, worker_id):
if worker_id == "master":
# not running with xdist, set up directly
data_dir = tmp_path_factory.mktemp("data")
populate_test_data(data_dir)
return data_dir
root_tmp = tmp_path_factory.getbasetemp().parent
lock = root_tmp / "data.lock"
data_dir = root_tmp / "shared_data"
with FileLock(str(lock)):
if not data_dir.exists():
data_dir.mkdir()
populate_test_data(data_dir)
return data_dirThis requires the filelock package (uv add --dev filelock). The populate_test_data function is whatever setup your tests need (creating files, seeding a database, etc.).
Controlling test distribution
By default, pytest-xdist distributes tests to available workers using load balancing (--dist load). It sends pending tests in chunks to whichever worker is free. The most useful alternative modes:
--dist loadscopegroups tests by module or class, keeping them on the same worker. This avoids redundant setup when tests share module- or class-scoped fixtures.--dist loadfilegroups tests from the same file on one worker. Similar toloadscopebut scoped to files only.--dist workstealstarts with an even split, then lets idle workers steal tests from busy ones. Good for suites with uneven test durations.
uv run pytest -n auto --dist loadscopeSee the pytest-xdist distribution docs for the full list of modes.
Measuring the speedup
Compare sequential and parallel run times:
uv run pytest --durations=10 # sequential, shows slowest tests
uv run pytest -n auto --durations=10 # parallelIf the speedup is less than expected, check for a few slow tests that dominate total time. Parallelism helps most when many tests take roughly the same amount of time.
See also
Also Mentioned In
Get Python tooling updates
Subscribe to the newsletter