
Pytest Dev
- 10 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
Pytest-dev is a Claude Code skill for writing, refactoring, and speeding up pytest suites and tuning Python test CI.
About
Pytest-dev is a Claude Code skill for Python testing that writes and refactors pytest suites, fixes flakiness, designs fixtures and markers, and speeds up test collection and runtime. A developer uses it for pytest best practices, pytest 9.x features, plugin selection, and CI tuning such as GitHub Actions sharding and xdist parallelism. It ships reference guides and helper scripts for profiling slow tests and splitting suites by historical timings.
- Writes low-flake pytest suites and designs fixtures, markers, and parametrization
- Profiles and speeds up test collection and runtime with --durations and xdist
- Tunes CI with GitHub Actions sharding, parallelism, and JUnit artifacts
Pytest Dev by the numbers
- 10 all-time installs (skills.sh)
- Ranked #1,550 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
pytest-dev capabilities & compatibility
Free; uses Python, pytest, and standard plugins.
- Capabilities
- test authoring · flake fixing · ci tuning
- Works with
- github
- Use cases
- testing · ci cd · debugging
- IDEs
- pycharm · vscode
- Pricing
- Free
What pytest-dev says it does
Produce **high-signal, low-flake, fast** pytest suites and CI configs, with an explicit focus on **measurable wins** (runtime, flake rate, coverage quality).
Parallelize on one machine (xdist): `python3 -m pytest -n auto --dist load`
Shard in CI** (split test files by historical timings; keep shards balanced).
npx skills add https://github.com/bjornmelin/dev-skills --skill pytest-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Write low-flake, fast pytest suites and tune Python test CI with sharding, parallelism, and coverage.
Who is it for?
Writing low-flake pytest suites, designing fixtures and markers, fixing flakiness, and optimizing test runtime and CI.
Skip if: Non-Python test frameworks or general application code outside the test suite.
When should I use this skill?
Asked about pytest best practices, pytest 9.x features, pytest plugins, or test performance and CI tuning.
What you get
High-signal, low-flake, fast pytest suites and CI configs with measurable wins in runtime, flake rate, and coverage quality.
- low-flake pytest test suites
- fixture and marker design
- CI configs with sharding and parallelism
By the numbers
- 5-step default workflow
- 5-step high-ROI optimization playbook
- 3 bundled helper scripts
Files
pytest-dev
Produce high-signal, low-flake, fast pytest suites and CI configs, with an explicit focus on measurable wins (runtime, flake rate, coverage quality).
Default workflow
1. Classify the tests
- Unit: pure functions, no I/O (preferred)
- Integration: DB/filesystem/multiprocess, slower but valuable
- System/E2E: external services or UI, keep minimal and well-gated
2. Identify boundaries
- Time/clock, randomness, network, filesystem, DB, env vars, global state
3. Pick the lightest seam
- Prefer fakes/stubs over deep mocks; prefer dependency injection over
patching internals 4. Make it deterministic
- Control time, seeds, tmp dirs; avoid order dependencies
5. Measure before optimizing
- Collection time vs runtime; quantify with
--durations+ a single baseline
6. Harden for CI
- Enforce marker discipline, strict config, timeouts, isolation for parallel
Quick commands
Use python3 by default. If the project uses uv, prefer uv run python.
- Smallest repro:
python3 -m pytest path/to/test_file.py -q - First failure only:
python3 -m pytest -x --maxfail=1 - Find slow tests:
python3 -m pytest --durations=20 --durations-min=0.5 - Emit JUnit for CI:
python3 -m pytest --junitxml=reports/junit.xml - Parallelize on one machine (xdist):
python3 -m pytest -n auto --dist load
Optimization playbook (high ROI)
1. Reduce collection scope (testpaths, norecursedirs, avoid importing heavy modules at import time). 2. Fix fixture scoping (move expensive setup up-scope; ensure isolation). 3. Eliminate sleeps and retries (poll with timeouts; mock time). 4. Parallelize safely (xdist; isolate worker resources: tmp/db ports). 5. Shard in CI (split test files by historical timings; keep shards balanced).
Use the bundled references
Read these when needed (keep SKILL.md lean):
references/pytest_core.md: fixtures, markers, parametrization, strict mode,
TOML config, subtests (pytest 9.x).
references/plugins.md: plugin selection + usage patterns.references/performance.md: collection/runtime profiling and speedups.references/ci_github_actions.md: sharding, artifacts, caching, concurrency.
Use the bundled scripts
scripts/junit_slowest.py: report slowest tests/files from JUnit XML.scripts/junit_split.py: split test files into N shards using JUnit timings.scripts/run_pytest_filelist.py: run pytest for a list of test files.
Quality gates
- Tests pass in a clean environment (no hidden dependency on local state).
- No network/time dependency without explicit control.
- Parallel-safe or explicitly marked/serialized.
- CI emits machine-readable artifacts when relevant (JUnit, coverage).
GitHub Actions: pytest throughput patterns
Table of contents
- Baseline CI command shape
- Dependency caching (uv)
- Test sharding (matrix)
- Artifacts (JUnit, coverage)
- Fail-fast and cancellation
Baseline CI command shape
Prefer explicit module execution:
python3 -m pytest -q --junitxml=reports/junit.xmlAdd coverage only when required (coverage can add overhead):
python3 -m pytest -q --cov --cov-report=xml:coverage.xmlDependency caching (uv)
If your project uses uv, prefer astral-sh/setup-uv with caching enabled. Keep the cache key tied to your lockfile (e.g., uv.lock) for correctness.
Key ideas:
- cache the
uvdownload/build cache - avoid re-resolving dependencies on every run
Test sharding (matrix)
Sharding gives horizontal scaling (multiple runners). Combine with xdist for vertical scaling (multi-core per runner).
Baseline approach: 1. Run the full suite once and persist reports/junit.xml as an artifact. 2. Next runs use that JUnit as historical timing input for sharding.
This skill ships scripts/junit_split.py which prints the list of files for a given shard index:
python3 .codex/skills/pytest-dev/scripts/junit_split.py \
--junitxml reports/junit.xml \
--glob 'tests/**/*.py' \
--groups 4 \
--index 0Then run just those files:
python3 .codex/skills/pytest-dev/scripts/run_pytest_filelist.py shard_0.txt \
-- -q --junitxml=reports/junit.xmlPractical tip: shard by file, then use xdist within the shard:
python3 -m pytest -n auto --dist loadfile ...
Artifacts (JUnit, coverage)
Always upload artifacts when debugging flakes/perf:
reports/junit.xmlcoverage.xml/htmlcov/(if enabled)
JUnit is also used by many “annotate test failures” Actions.
Fail-fast and cancellation
CI resource best practices:
- Use GitHub Actions
concurrencyto cancel obsolete runs on the same branch. - For sharded jobs, set
fail-fast: falseif you want all shards’ failures in
one run; otherwise keep it true for faster feedback.
Performance and flake reduction playbook
Table of contents
- Measure first
- Collection speed
- Runtime speed
- xdist strategy selection
- CI sharding
- Flakiness checklist
Measure first
Establish a baseline before changing anything:
- Whole suite: run the canonical CI command locally once.
- Slowest tests:
python3 -m pytest --durations=20 --durations-min=0.5
Separate the problem:
- Collection time (imports, discovery, plugin overhead)
- Runtime (fixtures, I/O, algorithmic cost)
Collection speed
High-ROI fixes:
- Set
testpathsto avoid scanning the repo. - Add
norecursedirsfor non-test directories with lots of files. - Move heavy imports behind runtime boundaries (lazy import inside functions).
- Disable unneeded plugins (built-in or third-party) for the suite you’re running.
Debugging collection overhead:
- Use Python import timing:
python3 -X importtime -m pytest ...(noisy but useful).
Runtime speed
Fixture optimization:
- Keep fixtures small; avoid “do everything” fixtures.
- Use narrow scopes by default; increase scope only when safe.
- Avoid per-test DB schema creation; isolate per-worker, not per-test.
Avoid sleeps:
- Replace
time.sleep()with polling + a timeout. - Prefer fake clocks when testing time-based logic.
Mock slow boundaries:
- network calls
- filesystem or external CLI calls
- slow cryptography/compression
xdist strategy selection
Start with:
-n auto --dist loadfor broad suites with many independent tests.
Consider:
--dist loadfilewhen tests in the same file share expensive setup that is
safe to reuse per worker.
--dist loadscopeto keep classes/modules together (helps expensive fixtures).--dist workstealwhen you have a few very slow tests and lots of fast ones.
Common xdist gotchas:
- Each worker performs full collection (collection is multiplied).
- Global resources must be namespaced per worker (DB names, ports, tmp dirs).
CI sharding
If the suite still exceeds your CI budget:
1. Emit JUnit XML in CI: --junitxml=reports/junit.xml 2. Use historical timings to split files across a matrix:
- This skill’s
scripts/junit_split.pycan shard by file.
3. Inside each shard, optionally use xdist for per-runner parallelism:
--dist loadfilecomplements file-level sharding well.
Flakiness checklist
Flakes usually come from:
- time (real clock, race conditions)
- random (non-seeded generators)
- order dependence (shared global state)
- async concurrency (tasks not awaited, event loop leakage)
- shared external resources (ports, tmp dirs, DBs)
Mitigations:
- Make shared resources per-worker (xdist) and per-test (
tmp_path) as needed. - Enforce timeouts (
pytest-timeout) to prevent hangs. - Randomize order (
pytest-randomly) to expose hidden coupling (then fix).
Plugins and helper libraries (selection guide)
Prefer a small, curated plugin set. Each plugin adds hooks, potential incompatibilities, and runtime overhead.
Table of contents
- Core set (most projects)
- Parallelism and sharding
- Coverage
- Async
- HTTP/network mocking
- Flake reduction
- Performance tooling
Core set (most projects)
pytest-mock: ergonomicunittest.mockusage (mockerfixture).pytest-cov: coverage integration (--cov, reports, fail-under).
Parallelism and sharding
pytest-xdist: parallelize on one machine (-n auto) and choose a
distribution strategy (--dist).
Sharding across CI machines:
- Prefer a purpose-built sharding plugin (e.g.,
pytest-split) when you can. - Otherwise shard by test files using historical timings (see this skill’s
scripts/junit_split.py).
Coverage
pytest-cov:- avoid “coverage theater”: focus on meaningful paths and invariants
- for parallel runs, ensure you combine coverage data correctly (depends on
your runner/sharding strategy)
Async
pytest-asyncio:- centralize event-loop policy in config/fixtures
- keep async tests explicit; don’t mix sync/async implicitly via autouse
HTTP/network mocking
Choose one per stack:
pytest-httpxforhttpxresponsesforrequests(not pytest-specific but widely used)respxforhttpx(alternative)vcrpywhen you intentionally record/replay HTTP (use sparingly; can hide
bugs and break determinism if recordings drift)
Flake reduction
pytest-timeout: hard cap on per-test runtime (prevents deadlocks hanging CI).pytest-rerunfailures: last resort for quarantining flakes while fixing root
causes (keep reruns small and time-bounded).
pytest-randomly: randomize order and seed RNG to expose hidden coupling.
Performance tooling
pytest-benchmark: microbenchmarks with a stable fixture and comparisons.pytest-profiling/pytest-monitor: deeper profiling when--durations
isn’t enough.
pytest core (pytest 9.x)
This reference focuses on pytest itself (not plugins): configuration, fixtures, markers, parametrization, subtests, and strictness.
Table of contents
- Configuration (INI vs TOML)
- Markers and selection
- Fixtures and scoping
- Parametrization (and IDs)
- Subtests (pytest 9.0+)
- Warnings, xfail/skip, and strictness
Configuration (INI vs TOML)
Prefer pyproject.toml
Legacy (INI-compat) config lives in pyproject.toml under:
[tool.pytest.ini_options]
addopts = "-ra -q"
testpaths = ["tests"]pytest 9.0 adds native TOML config under [tool.pytest] (native TOML types instead of INI-compat string parsing):
[tool.pytest]
minversion = "9.0"
addopts = ["-ra", "-q"]
testpaths = ["tests"]Important:
[tool.pytest]and[tool.pytest.ini_options]cannot be used together.- If using a separate config file (
pytest.toml/.pytest.toml), use[pytest].
High-ROI config keys
testpaths: avoid scanning the whole repo during collection.norecursedirs: exclude heavy dirs (.git,.venv,node_modules, caches).addopts: set defaults for CI (e.g.,-ra,--strict-markers).markers: document all custom markers for strict validation.filterwarnings: enforce deprecations, silence known-noisy libs.
Markers and selection
Define markers (then enforce strictness)
In config:
[tool.pytest.ini_options]
markers = [
"unit: fast isolated tests",
"integration: hits DB/filesystem or other services",
"system: end-to-end tests",
]Then select:
-m "unit"/-m "not integration"for suites.- Combine:
-m "unit and not slow".
Strict marker enforcement:
strict_markers = true(or enable strict mode; see below).
Fixtures and scoping
Fixture rules of thumb
- Prefer function-scoped fixtures by default.
- Increase scope (
module/session) only when: - setup is expensive, and
- the resource is safe to share, and
- tests stay isolated (no cross-test leakage).
Avoid anti-patterns:
- giant
autouse=Truefixtures that implicitly mutate global state. - session fixtures that return mutable objects shared across tests.
Built-in fixtures you should reach for first
tmp_path: per-test temp dir (safe for parallel runs).monkeypatch: env vars, module attributes,sys.path, etc.capsys/capfd: capture stdout/stderr.caplog: capture log records for assertions.request: introspection + dynamic fixture access (request.getfixturevalue).
Parametrization (and IDs)
Prefer @pytest.mark.parametrize for static matrices
Use parametrization when the input matrix is known at collection time.
IDs:
- Provide stable ids (debuggable CI).
- pytest 9 adds
strict_parametrization_idsto error on duplicate ids
instead of auto-disambiguating.
Subtests (pytest 9.0+)
Subtests are a good fit when the iteration set is only known at runtime (e.g., scanning files on disk, introspecting plugins, dynamic resources).
Pattern:
- Accept the
subtestsfixture (typepytest.Subtests). - Wrap each case in
with subtests.test(...):.
Design guidance:
- Subtests complement parametrization; don’t replace parametrization for static
matrices.
- Keep the per-subtest body small; heavy work should live outside the context.
Warnings, xfail/skip, and strictness
Strict mode (pytest 9.0+)
strict = true enables:
strict_configstrict_markersstrict_parametrization_idsstrict_xfail
You can override individual strictness options explicitly even when strict is on.
Note: strict mode can enable new options in future pytest releases; only turn it on if you pin/lock pytest or you want that behavior.
Deprecations in pytest 9
pytest 9 turns PytestRemovedIn9Warning into errors by default; update your suite/plugins or temporarily silence with a warning filter (as a stopgap only).
xfail discipline
Prefer:
- fix the root cause, or
- quarantine with a clearly-scoped marker + follow-up, or
xfailwith a link and a narrow condition.
If using strict xfail (strict_xfail), an unexpected pass becomes a failure (useful to ensure quarantines get removed).
#!/usr/bin/env python3
"""Report slowest pytest tests/files from JUnit XML.
This is meant for CI and local triage:
- Identify slow test cases / files.
- Fail the run if a single test (or file aggregate) exceeds a threshold.
Works with JUnit XML generated by: `python3 -m pytest --junitxml=...`.
"""
from __future__ import annotations
import argparse
import statistics
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
@dataclass(frozen=True)
class _Testcase:
name: str
classname: str | None
file: str | None
seconds: float
def _iter_xml_paths(path: Path) -> Iterable[Path]:
if path.is_dir():
yield from sorted(p for p in path.rglob("*.xml") if p.is_file())
return
yield path
def _safe_float(value: str | None) -> float:
if value is None:
return 0.0
try:
return float(value)
except ValueError:
return 0.0
def _guess_file_from_classname(classname: str) -> str | None:
# pytest usually sets classname like "tests.test_mod" or
# "tests.test_mod.TestClass".
# Prefer full module path up to first class name: keep all but last segment
# if it looks like a class (starts with uppercase).
parts = classname.split(".")
if len(parts) >= 2 and parts[-1][:1].isupper():
parts = parts[:-1]
# If it already looks like a path, keep it.
joined = "/".join(parts)
if "/" in joined and joined.endswith(".py"):
return joined
if not joined:
return None
return f"{joined}.py"
def _testcase_id(case: _Testcase) -> str:
if case.classname:
return f"{case.classname}::{case.name}"
return case.name
def _file_id(case: _Testcase) -> str | None:
if case.file:
return case.file
if case.classname:
return _guess_file_from_classname(case.classname)
return None
def _parse_junit_xml(xml_path: Path) -> list[_Testcase]:
try:
root = ET.parse(xml_path).getroot()
except (ET.ParseError, OSError) as exc:
raise RuntimeError(f"Failed to parse {xml_path}: {exc}") from exc
out: list[_Testcase] = []
for testcase in root.iter("testcase"):
name = testcase.get("name") or ""
classname = testcase.get("classname")
file_attr = testcase.get("file")
seconds = _safe_float(testcase.get("time"))
out.append(
_Testcase(
name=name,
classname=classname,
file=file_attr,
seconds=seconds,
)
)
return out
def _print_rows(rows: list[tuple[str, float]], top: int) -> None:
if not rows:
print("No test timings found.")
return
shown = rows[:top] if top > 0 else rows
width = max(len(name) for name, _ in shown)
for name, seconds in shown:
print(f"{name:<{width}} {seconds:9.3f}s")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--junitxml",
required=True,
type=Path,
help="JUnit XML file or directory containing XML files.",
)
parser.add_argument(
"--mode",
choices=("testcase", "file"),
default="testcase",
help="Report slowest individual testcases or aggregate by file.",
)
parser.add_argument(
"--top",
type=int,
default=20,
help="Number of rows to print (0 = all).",
)
parser.add_argument(
"--min-seconds",
type=float,
default=0.0,
help="Ignore entries below this duration.",
)
parser.add_argument(
"--fail-over",
type=float,
default=None,
help="Exit non-zero if any entry exceeds this many seconds.",
)
args = parser.parse_args(argv)
xml_paths = list(_iter_xml_paths(args.junitxml))
if not xml_paths:
print(f"No XML files found under: {args.junitxml}", file=sys.stderr)
return 2
testcases: list[_Testcase] = []
for xml_path in xml_paths:
testcases.extend(_parse_junit_xml(xml_path))
if args.mode == "testcase":
rows = [
(_testcase_id(c), c.seconds)
for c in testcases
if c.seconds >= args.min_seconds
]
else:
by_file: dict[str, float] = {}
for c in testcases:
file_id = _file_id(c)
if not file_id:
continue
if c.seconds < args.min_seconds:
continue
by_file[file_id] = by_file.get(file_id, 0.0) + c.seconds
rows = list(by_file.items())
rows.sort(key=lambda r: r[1], reverse=True)
_print_rows(rows, top=args.top)
if args.fail_over is not None:
worst = rows[0][1] if rows else 0.0
if worst > args.fail_over:
return 1
if rows:
values = [s for _, s in rows]
p50 = statistics.median(values)
p95 = statistics.quantiles(values, n=20)[18] if len(values) >= 20 else None
print("")
print(f"count={len(values)} p50={p50:.3f}s", end="")
if p95 is not None:
print(f" p95~={p95:.3f}s")
else:
print("")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Split test files into N shards using historical JUnit XML timings.
Goal: make GitHub Actions (or any CI matrix) shards finish at roughly the same
time by assigning *test files* to each shard based on measured durations.
This script intentionally shards by file (not individual nodeids) because JUnit
XML often lacks a stable pytest nodeid. Sharding by file is usually a strong
baseline and works well with `pytest -n auto --dist loadfile`.
"""
from __future__ import annotations
import argparse
import statistics
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
def _iter_xml_paths(path: Path) -> Iterable[Path]:
if path.is_dir():
yield from sorted(p for p in path.rglob("*.xml") if p.is_file())
return
yield path
def _safe_float(value: str | None) -> float:
if value is None:
return 0.0
try:
return float(value)
except ValueError:
return 0.0
def _guess_file_from_classname(classname: str) -> str | None:
# Usually looks like: "tests.test_mod" or "tests.test_mod.TestClass".
parts = classname.split(".")
if len(parts) >= 2 and parts[-1][:1].isupper():
parts = parts[:-1]
joined = "/".join(parts)
if not joined:
return None
return f"{joined}.py"
def _timings_by_file(xml_paths: Iterable[Path]) -> dict[str, float]:
totals: dict[str, float] = {}
for xml_path in xml_paths:
try:
root = ET.parse(xml_path).getroot()
except (ET.ParseError, OSError) as exc:
raise RuntimeError(f"Failed to parse {xml_path}: {exc}") from exc
for testcase in root.iter("testcase"):
seconds = _safe_float(testcase.get("time"))
if seconds <= 0:
continue
file_id = testcase.get("file")
if not file_id:
classname = testcase.get("classname")
if classname:
file_id = _guess_file_from_classname(classname)
if not file_id:
continue
totals[file_id] = totals.get(file_id, 0.0) + seconds
return totals
@dataclass(frozen=True)
class _WeightedFile:
path: str
seconds: float
def _glob_files(globs: list[str]) -> list[str]:
out: list[str] = []
for pattern in globs:
out.extend(str(p) for p in sorted(Path().glob(pattern)) if p.is_file())
# De-dupe while preserving order.
seen: set[str] = set()
deduped: list[str] = []
for p in out:
if p in seen:
continue
seen.add(p)
deduped.append(p)
return deduped
def _binpack(
weighted: list[_WeightedFile], groups: int
) -> list[list[_WeightedFile]]:
# Greedy largest-first bin packing.
bins: list[list[_WeightedFile]] = [[] for _ in range(groups)]
totals = [0.0 for _ in range(groups)]
for item in sorted(weighted, key=lambda w: w.seconds, reverse=True):
idx = min(range(groups), key=lambda i: totals[i])
bins[idx].append(item)
totals[idx] += item.seconds
return bins
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--junitxml",
required=True,
type=Path,
help="JUnit XML file or directory containing XML files.",
)
parser.add_argument(
"--glob",
action="append",
default=[],
help="Glob for test files (repeatable). Example: --glob 'tests/**/*.py'",
)
parser.add_argument(
"--groups",
type=int,
required=True,
help="Number of shards/groups.",
)
parser.add_argument(
"--index",
type=int,
default=None,
help="If set, print only this shard's file list (0-based).",
)
parser.add_argument(
"--out-dir",
type=Path,
default=None,
help="If set, write shard_0.txt..shard_{n-1}.txt into this directory.",
)
args = parser.parse_args(argv)
if args.groups < 1:
print("--groups must be >= 1", file=sys.stderr)
return 2
if args.index is not None and (args.index < 0 or args.index >= args.groups):
print("--index must be within [0, groups)", file=sys.stderr)
return 2
xml_paths = list(_iter_xml_paths(args.junitxml))
if not xml_paths:
print(f"No XML files found under: {args.junitxml}", file=sys.stderr)
return 2
files = _glob_files(args.glob) if args.glob else []
if not files:
print("No test files matched. Provide at least one --glob.", file=sys.stderr)
return 2
timings = _timings_by_file(xml_paths)
known = list(timings.values())
# If the timing corpus is tiny (e.g., a partial report), using its median as
# the default can massively overweight unknown files. Prefer a conservative
# default until we have a meaningful sample size.
default_seconds = statistics.median(known) if len(known) >= 50 else 1.0
weighted: list[_WeightedFile] = []
for f in files:
seconds = timings.get(f)
if seconds is None:
# JUnit may use module-ish paths; try a best-effort normalization.
alt = f.replace("\\", "/")
seconds = timings.get(alt, default_seconds)
weighted.append(_WeightedFile(path=f, seconds=seconds))
bins = _binpack(weighted, groups=args.groups)
if args.out_dir is not None:
args.out_dir.mkdir(parents=True, exist_ok=True)
for i, shard in enumerate(bins):
out_path = args.out_dir / f"shard_{i}.txt"
out_path.write_text("".join(f"{w.path}\n" for w in shard), encoding="utf-8")
if args.index is not None:
for w in bins[args.index]:
print(w.path)
return 0
# Print a short summary for humans.
totals = [sum(w.seconds for w in shard) for shard in bins]
for i, total in enumerate(totals):
print(f"shard {i}: {total:.2f}s ({len(bins[i])} files)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Run pytest for a newline-delimited list of test files.
Useful for CI sharding where a prior step produces `shard_N.txt` containing a
list of test files to execute.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
def _read_filelist(path: Path) -> list[str]:
files: list[str] = []
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
files.append(line)
return files
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("filelist", type=Path, help="Path to shard_N.txt file.")
parser.add_argument(
"--python",
default=sys.executable,
help="Python interpreter to run pytest with.",
)
parser.add_argument(
"pytest_args",
nargs=argparse.REMAINDER,
help="Extra pytest args (prefix with -- to separate).",
)
args = parser.parse_args(argv)
files = _read_filelist(args.filelist)
if not files:
print(f"No files in: {args.filelist}", file=sys.stderr)
return 2
pytest_args = args.pytest_args
if pytest_args and pytest_args[0] == "--":
pytest_args = pytest_args[1:]
cmd = [args.python, "-m", "pytest", *files, *pytest_args]
return subprocess.call(cmd)
if __name__ == "__main__":
raise SystemExit(main())
Related skills
FAQ
How does pytest-dev speed up slow suites?
It reduces collection scope, fixes fixture scoping, eliminates sleeps, parallelizes with xdist, and shards CI by historical timings.
Does it cover pytest 9.x features?
Yes, including subtests, strict mode, and TOML config, documented in the bundled references.