From pytest-consistency
Write, review, refactor, or debug Python tests with pytest (fixtures, parametrize, conftest.py, marks, tmp_path, monkeypatch, pytest.raises) using one canonical, native idiom set. Use this skill whenever code adds or fixes tests in a pytest project, migrates unittest-style tests, organizes fixtures, or when the user hits "fixture not found", tests silently not collected, PytestUnknownMarkWarning, a for-loop test that stops at the first failure, or asks how to test exceptions or temporary files. Trigger it even when the user just says "write tests for this function" in a Python project — without saying the word "pytest."
How this skill is triggered — by the user, by Claude, or both
Slash command
/pytest-consistency:pytest-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
pytest is stable, but training data is saturated with unittest style, so generated tests
pytest is stable, but training data is saturated with unittest style, so generated tests
import TestCase into pytest projects, hand-roll setup/teardown, loop over cases inside
one test, and try/except around code that should use pytest.raises. This skill pins the
native pytest idiom set — pytest 8 semantics — so tests are flat, parametrized, and
fixture-driven.
| Always | Never | Why |
|---|---|---|
plain assert result == expected | self.assertEqual(...) / assertTrue | Assertion rewriting gives rich diffs; unittest methods need a TestCase class and add noise. |
| fixtures (parameter injection) | setUp/tearDown methods or module globals | Fixtures are scoped, composable, and only run for tests that request them. |
@pytest.fixture with yield for teardown | addCleanup / try/finally in every test | Teardown runs even on failure, lives next to setup. |
@pytest.mark.parametrize("a,b", [(1, 2), (3, 4)]) | a for loop over cases inside one test | The loop is one test that stops at the first failure; parametrize reports every case. |
with pytest.raises(ValueError, match=r"negative"): | try: ...; except ValueError: pass (or bare pytest.raises(Exception)) | The try form passes when nothing raises; matching the message pins the right error. |
tmp_path | tempfile boilerplate or the legacy tmpdir | tmp_path is a pathlib.Path, auto-cleaned, per-test. |
monkeypatch.setenv/setattr | manually saving/restoring attributes or os.environ | Undo is automatic and exception-safe. |
pytest.approx(0.3) | round(x, 5) == round(y, 5) float hacks | approx handles tolerance correctly, including in collections. |
functions in test_*.py, grouped by plain classes only when it helps (no __init__) | TestCase inheritance for grouping | A class with __init__ is silently not collected. |
custom marks registered in config + --strict-markers | ad-hoc @pytest.mark.slowtypo | Unregistered marks are typo-prone; strict mode makes them errors. |
House style:
import pytest
from billing import compute_fee
@pytest.fixture
def account(db):
acc = db.create_account(balance=100)
yield acc
db.delete(acc)
@pytest.mark.parametrize(
("amount", "expected"),
[(0, 0), (100, 1), (250, 3)],
ids=["zero", "boundary", "rounded-up"],
)
def test_compute_fee(account, amount, expected):
assert compute_fee(account, amount) == expected
def test_compute_fee_rejects_negative(account):
with pytest.raises(ValueError, match=r"amount must be >= 0"):
compute_fee(account, -5)
test_*.py/*_test.py, functions test_*,
classes Test* without __init__. A helper named test_helper gets collected; a
misnamed tests_login silently never runs.pytest.raises block's raising line never executes — put follow-up
assertions on excinfo (excinfo.value.args) after the with block.session-scoped fixture holding mutable state couples tests;
widen scope only for genuinely immutable/expensive resources (DB engine yes, DB rows no).conftest.py is an anti-pattern; conftest is auto-discovered for
fixtures/hooks, shared helpers belong in a real importable module.@pytest.mark.parametrize on a fixture-requesting argument mixing up direct vs
indirect= silently passes the raw value through.xfail without strict=True quietly hides tests that started passing; set
xfail_strict = true in config.pytest-asyncio (asyncio_mode = "auto") or anyio explicitly.Target pytest 8 (idioms identical in 7). Long-removed-but-still-generated: yield-based
test functions, pytest.config global, out-of-band setup.cfg quirks. Prefer
pyproject.toml configuration ([tool.pytest.ini_options]). The legacy tmpdir (py.path)
still exists but tmp_path is the canonical spelling.
conftest.py up the tree
and pytest --fixtures before writing one).ids=; one behavior per test function.pytest.raises(..., match=...); floats via approx; env/attrs via
monkeypatch; files via tmp_path; output via capsys; logs via caplog.--strict-markers/xfail_strict; mark slow/integration tests
for selection with -m.test_
prefix, fixtures with hidden cross-test state.For the fuller fixture-scope/ordering reference, parametrization recipes (stacking,
indirect, fixture params), built-in fixture catalog, and configuration template, read
references/pytest-patterns.md.
Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub guidogl/pytest-consistency --plugin pytest-consistency