Parametrization and selection¶
There is exactly one way to declare cases: @pytest.mark.parametrize.
The plugin adds no declaration syntax of its own — it changes what a
parametrized function means (every combination becomes a fraction
row sampled N times) without changing how you write it.
Axes and ids¶
Every decorator is an axis; stacked decorators cross-product. Ids
compose exactly like pytest’s: bottom decorator leftmost, top varies
fastest, pytest.param(id=...) and ids= override per value.
import pytest
@pytest.mark.parametrize("style", ["terse", "chain_of_thought"])
@pytest.mark.parametrize("text", [
pytest.param("my card was charged twice", id="refund"),
])
def bench_triage(text, style):
answer = my_classifier(text, prompt_style=style)
assert answer == "billing"
triage::refund-terse 8/10 $0.0010 FLAKY
triage::refund-chain_of_thought 10/10 $0.0040
Two rows, one comparison: the cheap prompt is a coin flip, the expensive one holds — with the price difference right next to it.
Row identity is the function’s short name plus the full composed id
(triage::refund-terse); items are
bench_x.py::bench_triage::refund-terse[run3].
Which axis is “the case”?¶
Whichever you want — there is no privileged axis. A useful convention:
put the inputs on one axis with explicit id=s, and the
configurations you are comparing (models, prompts, thresholds) on
their own axes with value-derived ids. The composed id then reads
<input>-<config>, and the summary rows group naturally.
Marks¶
Marks are pytest’s selection metadata, and they work on bench items exactly as on tests:
@pytest.mark.parametrize("text,expected", [
pytest.param("hallo", "greeting", id="german",
marks=[pytest.mark.eu, pytest.mark.slow]), # one case
pytest.param("bad case", "x", id="broken",
marks=pytest.mark.skip(reason="not yet")), # skip one case
])
@pytest.mark.llm # every case
def bench_classify(text, expected): ...
skip/skipif/xfailbehave exactly as in pytest; skipped items never enter the fractions.Custom marks select with
-m:-m "llm and eu",-m "not slow".Register custom marks in your pytest config to avoid unknown-mark warnings (and use
--strict-markersto make typos fail):[pytest] markers = fast: quick cases, safe for pre-commit slow: expensive cases, nightly only
Note
indirect=True is not supported: it routes parameters through
fixtures, and bench items do not participate in pytest’s fixture
system. See Internals for the reasoning.
Selecting cases¶
Item ids contain the composed case id, so pytest’s whole selection toolbox applies with no plugin-specific filters:
pytest benchmarks/ -k identify_pii # one case, all runs
pytest benchmarks/ -k bench_triage # one benchmark function
pytest benchmarks/ -k "refund and not terse" # expressions over ids
pytest benchmarks/ -m "fast and not eu" # marks
pytest "benchmarks/bench_x.py::bench_triage::refund-terse[run3]" # one run
One sharp edge inherited from pytest: the -k expression grammar
accepts brackets and dashes but rejects = — select on id
substrings and mark names, not key=value pairs. If a value must be
selectable, give it an id-visible axis (its value text lands in the
id) or a mark.