pytest-xspec
Executable specifications with pytest.
Link specifications to your existing pytest tests, organize your test suite your way, and see which specifications your test suite verifies. Detect missing tests and enforce verification in CI.
Why pytest-xspec?
As software engineers, we like code. Ask us what the software does, and we might say, "Just read the code." But stakeholders need a shared understanding of what the software should do. We turn to acceptance tests to make those expectations precise, using examples in the language of the business.
For many of us, making a whole test suite read as a clear specification is costly. It takes time and practice, and the work continues as the codebase grows. Related behavior spreads across files, implementation checks sit beside acceptance tests, and the overall requirements become harder to see.
pytest-xspec connects a readable overview of your requirements to the tests that verify them.
Keep specifications in a document your team can discuss, put tests where they make sense,
and link them with IDs. The report shows which requirements are verified, which remain
unverified and which have no tests. When business asks what is covered, you have an answer
they can read.
Usage
Install pytest-xspec into your pytest environment. The plugin is discovered
automatically. Python 3.10+ and pytest 8+ are supported.
pip install pytest-xspec
Write your specifications
Create xspec.toml in pytest's project root, usually the top-level project directory.
Keep each specification, its description and the reason it matters together in one document.
[[requirements]]
id = "REQ-001"
description = "Adding two integers returns their sum."
rationale = "Calculations must give predictable results."
[[requirements]]
id = "REQ-002"
description = "Adding zero leaves the value unchanged."
Use one [[requirements]] entry per specification. Each entry needs an id.
The description states the requirement, and the rationale explains why it matters.
Both are optional and can be empty strings. This text stays in the document,
while tests and reports refer only to the IDs.
See the specification document example for a complete runnable project.
For specifications maintained in other documents or tables, declare just their IDs.
Mark tests
Associate tests with specifications, using one marker per specification.
import pytest
@pytest.mark.xspec("REQ-001")
@pytest.mark.xspec("REQ-002")
def test_adding_zero_preserves_the_value() -> None:
assert 2 + 0 == 2
Run pytest
Plain pytest checks specification configuration and markers without printing
a specification report. Add --xspec-report to see the report, or --xspec-strict
to also fail the run when a specification is unverified.
pytest --xspec-report # Print the verification report
pytest --xspec-json=report.json # Export statuses and individual test outcomes
pytest --xspec-strict # Report and fail on unverified specifications
pytest -m xspec --xspec-strict # Run only marked specification tests
The report is green when all specifications are verified and red when verification fails. The first report shows the example above; the second shows a specification without tests.
Select specifications
Use --xspec-select=ID to run tests linked to one specification. Repeat the option
to select several specifications. A test linked to several selected IDs runs once.
pytest --xspec-select=REQ-001
pytest --xspec-select=REQ-001 --xspec-select=REQ-002
Each option takes one exact ID, including any spaces or commas. Quote IDs when the
shell requires it. Unmarked tests are excluded. Collection paths, -k and -m
further narrow the selection. An undeclared selected ID is a usage error.
Selecting only IDs without matching tests produces an empty selection.
Strict mode still evaluates every declared specification. Selecting one ID keeps
other specifications in the report and the coverage calculation. Excluded tests
remain DESELECTED, and tests shared with other specifications keep all their links.
With selection active, terminal reports and the JSON scope field show selected
IDs, collection paths, -k, -m and whether the run only discovers tests.
Discover tests without running them
Add --collect-only -q to list the selected test IDs, including parameterized cases.
pytest --xspec-select=REQ-001 --collect-only -q
Discovery runs neither fixtures nor test bodies. It still imports collected modules,
which need their normal dependencies. Reports mark discovered tests as SKIPPED,
so discovery does not verify their specifications. Strict mode fails when any
specification remains unverified. Without strict mode, an empty selection keeps
pytest's no-tests exit code, 5.
Read specification statuses
Reports label specifications as requirements.
A specification is verified when every test marked with its ID passes. One passing test is not enough if another test for the same specification fails or skips.
When those tests have different results, the report uses the first matching status in this table, reading from top to bottom.
| Status | Meaning |
|---|---|
ERROR |
A test's setup or cleanup failed. |
FAILED |
A test failed. |
XPASSED |
A test marked as an expected failure passed instead. |
XFAILED |
A test failed as expected. |
SKIPPED |
A test was skipped or never ran. |
DESELECTED |
A test was excluded by a filter such as -k or -m. |
PASSED |
Every test for the specification passed. |
For example, if one test passes and another skips, the specification is SKIPPED
and remains unverified. If one test fails and another skips, it is FAILED.
Missing and unfinished tests
Pytest first finds tests, then runs them. If it finds no marked tests for a
declared specification, that specification is MISSING. Tests that were found but
never ran are SKIPPED, including runs that only find tests or stop early.
If pytest encounters an error while finding tests, such as a broken import,
the verification check cannot succeed.
Tests without markers
Tests without a specification marker do not change specification statuses. They can still make pytest fail. In the duck example, the specification passes, but the unmarked cheese test fails the pytest run.
Check coverage
Coverage is the percentage of declared specifications that are verified. With no declared specifications and no errors while finding tests, the verification check succeeds and coverage is 0.0%.
Errors while finding tests make verification fail, even at 100% coverage. The report follows
pytest's color settings, so --color=no disables color.
For a complete report, let pytest find the whole test suite. With -k or -m,
the plugin still knows about the tests excluded by the filter and reports marked
ones as DESELECTED. If you run only one folder, it cannot see tests in other
folders.
Use an existing specification
If you maintain specifications elsewhere, declare their IDs in pyproject.toml
or pytest.ini. Your original document remains the source for descriptions and context.
The markers, commands and reports work the same way with either configuration format.
In pyproject.toml, use a list of IDs:
[tool.xspec]
requirements = ["REQ-001", "REQ-002"]
In pytest.ini, use one ID per line:
[pytest]
xspec =
REQ-001
REQ-002
How the source is chosen
An ID list in either configuration file takes precedence over xspec.toml.
Declaring IDs in both pyproject.toml and pytest.ini is an error.
Specifications from different sources are never merged.
When neither configuration file declares IDs, pytest-xspec reads xspec.toml
from the project root. Those configuration files can still contain other settings.
An explicitly empty ID list also takes precedence, so it declares zero specifications
even when xspec.toml contains entries. An unused xspec.toml is not read or validated.
Empty specifications and validation
IDs are strings and are used exactly as written. Duplicate IDs and markers referring
to undeclared IDs are rejected. In xspec.toml, supplied descriptions and rationales
must also be strings. Additional fields are ignored.
To declare zero specifications in xspec.toml, write requirements = [].
A document without requirements, including an empty file, is a configuration error.
Invalid documents produce an error before tests run.
If no source exists, there are zero declared specifications.
Runnable examples
Start with the pizza, then try the other examples to see what makes a specification pass or remain unverified.
Run the commands below from the repository root. Dependencies are installed automatically.
| Example | With --xspec-strict |
Exit code |
|---|---|---|
| Quickstart - Two slices of pizza | PASSED, 100% coverage |
0 |
| Specification document - Addition rules in TOML | PASSED, 100% coverage |
0 |
| Missing specification - A lost sock | MISSING, 50% coverage |
1 |
| Partial verification - Cat skips a meeting | SKIPPED, 0% coverage |
1 |
| Acceptance only - Duck versus cheese | PASSED with -m xspec |
0 |
Run an example
Run the Quickstart example, then replace quickstart with another example folder.
uv run --locked pytest examples/quickstart --xspec-strict
For specification_document, the requirements live in
xspec.toml.
Its pyproject.toml only sets test discovery, leaving the document as the requirement source.
For acceptance_only, add -m xspec to run only the marked test.
Without that filter, the unmarked test makes pytest fail.
Select and discover example tests
Use the same example command with --xspec-select=REQ-001 to select the pizza test.
Add --collect-only -q to discover it without running it:
uv run --locked pytest examples/quickstart --xspec-select=REQ-001 --collect-only -q
For missing_requirement, repeat --xspec-select=REQ-002 to select both declared IDs.
Only the existing sock test runs. With --xspec-strict, the missing sock still
makes verification fail.
Inspect the JSON report
Export the example results to see the specification status and individual test outcomes.
uv run --locked pytest examples/quickstart --xspec-json=examples/quickstart/report.json
Open examples/quickstart/report.json after running the command.
Verify all examples
Check that every example behaves as described.
uv run --locked pytest examples/test_examples.py -n 0
All six checks should pass, including checks for the examples that deliberately fail. The examples are also verified in CI.
Development disclosure
This package is developed by AI agents under human direction. See AI_DISCLOSURE.md for details on authorship, human review and repository workflows.
This README was written by AI.
Contributing
See CONTRIBUTING.md for setup, tests and pull requests.
Metadata
Release files for pytest-xspec 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pytest_xspec-0.7.0.tar.gz | 142.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_xspec-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 155.1 kB
Release files / pytest_xspec-0.7.0.tar.gz
| Download URL | pytest_xspec-0.7.0.tar.gz |
|---|---|
| Size | 142.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b7e79840c8bd4abdff64c29e4e4267cb8500fcc25f96837a10ee527ed840e45c
|
|
BLAKE2b-256 checksum How to use checksums |
cf04704a2eb94db1e1632ffcee706d5bb4ef08ffff52c43eacd3d741137fc35f
|
| 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 Oct 15, 2026.
Transparency logRelease files / pytest_xspec-0.7.0-py3-none-any.whl
| Download URL | pytest_xspec-0.7.0-py3-none-any.whl |
|---|---|
| Size | 12.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
89f42e8ababb55b1aaa37c51a00563551e4b63aa7b35549c2c50b8ebff44a443
|
|
BLAKE2b-256 checksum How to use checksums |
d665f812a0ea4957af3624dc9a2214d5aa395e53e14fa08de774a75ab2c16006
|
| 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 Oct 15, 2026.
Transparency logSecurity for pytest-xspec 0.7.0
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.
Attestation for pytest_xspec-0.7.0.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 Oct 15, 2026.
Transparency logA 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.