| name | pytest |
| description | The pytest testing framework and its Python-specific idioms — the things that set pytest apart from unittest and generic testing. Use when writing, running, debugging, or configuring Python tests with pytest — plain `assert` (rewritten for rich introspection), dependency-injected `@pytest.fixture` (scopes, autouse, yield-teardown, conftest sharing), table-driven `@pytest.mark.parametrize`, `skip`/`skipif`/`xfail` and custom markers, `pytest.raises`/`warns`/`approx`, builtin fixtures (`tmp_path`, `monkeypatch`, `capsys`, `caplog`), the CLI (`-k`/`-m`/`-x`/`--lf`/`--pdb`/node ids), config (`pyproject.toml`/`pytest.ini`/`pytest.toml`, `addopts`, `testpaths`, `--import-mode`), and plugins/hooks (`conftest.py`, `pytest_addoption`, xdist/cov/asyncio/mock). Includes inline `(pytest N.M+)` version annotations + a version-features lookup. The pytest framework — not unittest, not tox/nox runners, not coverage. |
pytest
Overview
pytest is the de-facto standard Python test framework. Its whole value proposition is
removing ceremony: a test is a plain function whose name starts with test_, a check is a
bare assert, and everything a test needs is requested by name and injected. There are no
TestCase subclasses to inherit, no setUp/tearDown to override, and no assertEqual
family to memorize — yet failures still report rich, introspected values.
The mental-model shift (vs. unittest):
You'd write in unittest… | In pytest you write… |
|---|
class T(unittest.TestCase): + methods | a plain module-level def test_x(): function |
self.assertEqual(a, b), assertTrue, assertRaises | assert a == b; with pytest.raises(...) |
setUp / tearDown / setUpClass | @pytest.fixture requested as a function argument |
| copy-paste a test body for each input | one test + @pytest.mark.parametrize |
@unittest.skip, manual skip logic | @pytest.mark.skip / skipif / xfail |
| shared base classes / mixins for setup | fixtures in conftest.py, auto-discovered |
The shift is from inheritance to composition-by-injection. Setup isn't a lifecycle method on a
base class; it's a fixture — a small function pytest runs for you and passes in. Tests declare
dependencies the way a function declares parameters, pytest resolves the graph, and teardown is
just the code after a yield. Reaching for fixtures + parametrize + marks instead of class
hierarchies and loops is the entire point.
Disambiguation — what this skill is NOT:
- Not
unittest — pytest runs existing unittest.TestCase and nose-style tests
unchanged, but this skill teaches the pytest-native style. (See references/configuration.md
for running mixed suites.)
- Not a test runner/orchestrator —
tox/nox manage environments and invoke pytest;
they're a layer above. pytest is what actually collects and runs tests.
- Not coverage — line/branch coverage is the
pytest-cov plugin (wraps coverage.py), not
core pytest. Mentioned in references/plugins-hooks.md.
- Not a mocking library — use the stdlib
unittest.mock (often via the pytest-mock
mocker fixture) or builtin monkeypatch. See references/builtin-fixtures.md.
- Not a generic testing tutorial — assumes you know what a unit test is; covers only what's
pytest-specific.
When to Use This Skill
| Reach for it when you need to… | pytest construct |
|---|
| Write a test that reports why it failed, with no boilerplate | plain def test_*() + bare assert |
| Provide setup/teardown and shared resources to tests | @pytest.fixture (+ yield teardown) |
| Reuse setup across files without import gymnastics | conftest.py (auto-discovered fixtures) |
| Run the same test over many inputs | @pytest.mark.parametrize |
| Assert an exception or warning is raised | pytest.raises(...) / pytest.warns(...) |
| Compare floats / nested numbers tolerantly | pytest.approx(...) |
| Skip, conditionally skip, or expect-fail a test | skip / skipif / xfail markers |
| Tag tests and run a subset | custom markers + -m, or -k name expr |
| A unique temp dir, env patching, output/log capture | tmp_path, monkeypatch, capsys, caplog |
| Re-run only failures, stop on first error, drop to a debugger | --lf, -x, --pdb |
| Configure discovery, default flags, warning filters | pyproject.toml / pytest.ini ini options |
| Extend collection/reporting or share options | plugins + hooks (conftest.py) |
Prerequisites
pip install -U pytest
pytest --version
Confirm the installed pytest version — it governs which features below are available:
pytest --version
python -c "import importlib.metadata as m; print(m.version('pytest'))"
Version note. pytest follows semantic versioning; new features land on minor and major
releases. The latest stable is 9.1.0 (2026-06-13); pytest requires Python 3.10+. Features
that have been stable since the pytest 6.x era or earlier are treated as bedrock and shown
here unannotated ("unlisted = long-standing"). Anything newer carries an inline (pytest N.M+)
tag — meaning "needs pytest N.M or later" — and only when sourced from the changelog. The full
feature → minimum-version map with citations is in
references/version-features.md. Pin a floor in config with
minversion (and required_plugins) so an too-old pytest fails fast instead of mis-parsing.
Core Workflows
Each snippet is runnable. Tags like (pytest 8.2+) mark the minimum version; untagged = bedrock.
1. Write a test
A test is a function named test_* in a file named test_*.py or *_test.py. Use a plain
assert; pytest rewrites asserts at import time so the failure shows the operands, not just
"AssertionError".
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
pytest
pytest test_sample.py::test_add
Tests may be grouped in a class TestThing: (no base class needed, must be Test-prefixed and
have no __init__) — useful for sharing fixtures/marks. Each test gets a fresh class instance.
2. Fixtures: setup by injection
A fixture is a function decorated with @pytest.fixture; request it by adding its name as a test
argument. yield splits setup from teardown (teardown runs even if the test fails).
import pytest
@pytest.fixture
def db():
conn = connect()
yield conn
conn.close()
def test_query(db):
assert db.query("select 1") == [1]
- Scopes control reuse/lifetime:
@pytest.fixture(scope="function"|"class"|"module"|"package"|"session") (default function).
autouse=True applies a fixture to every test in scope without it being requested.
- Put fixtures in
conftest.py to share them across a directory tree with zero imports.
- Fixtures can request other fixtures — pytest resolves the dependency graph.
Full coverage — finalizers, request, parametrized & factory fixtures, override precedence,
usefixtures — in references/fixtures.md; the builtin fixtures
(tmp_path, monkeypatch, capsys, caplog, cache, …) in
references/builtin-fixtures.md.
3. Parametrize: one test, many inputs
import pytest
@pytest.mark.parametrize("value, expected", [(2, 4), (3, 9), (4, 16)])
def test_square(value, expected):
assert value ** 2 == expected
Each row becomes its own test with a readable id (test_square[3-9]). Attach marks/ids to a single
case with pytest.param(6, 36, marks=pytest.mark.xfail, id="six"), and stack decorators to get
the cartesian product. More — indirect, custom ids=, pytest_generate_tests — in
references/parametrize.md.
4. Assert exceptions, warnings, and approximate equality
import pytest
def test_raises():
with pytest.raises(ValueError, match=r"must be positive"):
int_sqrt(-1)
def test_warns():
with pytest.warns(DeprecationWarning):
old_api()
def test_float():
assert 0.1 + 0.2 == pytest.approx(0.3)
pytest.raises(...) returns an ExceptionInfo (exc.value, exc.type). Details — match=,
exception groups, RaisesGroup, deprecated_call, approx tolerances — in
references/assertions.md.
5. Skip, skipif, xfail, and custom markers
import sys
import pytest
@pytest.mark.skip(reason="not implemented yet")
def test_todo(): ...
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
def test_posix(): ...
@pytest.mark.xfail(strict=True, reason="known bug #123")
def test_known_bug(): ...
@pytest.mark.slow
def test_big_import(): ...
Register custom markers so --strict-markers can catch typos:
[tool.pytest.ini_options]
markers = ["slow: marks tests as slow"]
Select by marker with -m "slow and not flaky". Full semantics (run=, raises=, runtime
pytest.skip/importorskip) in references/markers-skip-xfail.md.
6. Run and select tests
pytest -q
pytest -k "login and not slow"
pytest -m slow
pytest -x
pytest --maxfail=2
pytest --lf
pytest --ff
pytest -s
pytest -ra
pytest --pdb
pytest --durations=10
Node ids select precisely: pytest tests/test_api.py::TestAuth::test_login[user2]. Full flag tour
(traceback styles, --co, --stepwise, plugin toggles, exit codes) in
references/cli-usage.md.
7. Configure the project
pytest reads its config from the first matching file at the rootdir. Prefer pyproject.toml:
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers"
testpaths = ["tests"]
markers = ["slow: long-running tests"]
filterwarnings = ["error"]
pytest 9.0+ also supports a dedicated pytest.toml/.pytest.toml and a native-typed
[tool.pytest] table in pyproject.toml. Other sources: pytest.ini, tox.ini ([pytest]),
setup.cfg ([tool:pytest]). Discovery/rootdir rules, --import-mode (prepend/append/
importlib), pythonpath, and the full ini-option list are in
references/configuration.md.
Quick Reference
Untagged = bedrock (stable since pytest 6.x or earlier). Tags mark the minimum version.
| Task | Construct |
|---|
| Define a test | def test_*(): in test_*.py / *_test.py |
| Check a condition | bare assert expr (rewritten for rich output) |
| Setup/teardown by injection | @pytest.fixture + yield |
| Share fixtures across files | conftest.py |
| Fixture lifetime | @pytest.fixture(scope="module"|"session"|…) |
| Apply a fixture implicitly | @pytest.fixture(autouse=True) |
| Require a fixture without an arg | @pytest.mark.usefixtures("name") |
| Table-driven tests | @pytest.mark.parametrize("a,b", [...]) |
| Mark one parametrized case | pytest.param(..., marks=..., id=...) |
| Expect an exception | with pytest.raises(Exc, match=r"..."): |
| Expect a warning | with pytest.warns(Warning): |
| Approximate float/seq compare | pytest.approx(expected) |
| Skip / conditionally skip | @pytest.mark.skip / skipif(cond, reason=) |
| Expected failure | @pytest.mark.xfail(strict=True, raises=...) |
| Skip if import missing | pytest.importorskip("numpy") |
| Unique temp dir | tmp_path fixture (pathlib.Path) |
| Patch env/attr safely | monkeypatch fixture |
| Capture stdout/stderr | capsys / capfd fixture |
| Capture log records | caplog fixture |
| Select by name / marker | pytest -k EXPR / pytest -m EXPR |
| Re-run failures / stop early | pytest --lf / pytest -x |
| Read args from a file | pytest @args.txt (pytest 8.2+) |
| Default flags / discovery | addopts, testpaths (config file) |
| Dedicated config file | pytest.toml / [tool.pytest] (pytest 9.0+) |
| Parallelize | pytest -n auto (pytest-xdist plugin) |
| Coverage | pytest --cov=pkg (pytest-cov plugin) |
Troubleshooting
- "fixture 'x' not found" — the name is misspelled, the fixture lives outside the test's
conftest.py scope, or you forgot @pytest.fixture. Run pytest --fixtures to list what's
available where.
- A test passes alone but fails in the suite — shared state. Prefer function-scoped fixtures;
if you widened a scope (
module/session), the fixture's object is being reused/mutated.
assert shows no detail / "AssertionError" only — asserts in a non-test helper module
aren't rewritten; call pytest.register_assert_rewrite("mypkg.helpers") (see
references/assertions.md), or keep assertions in test modules.
- Custom marker warning ("Unknown pytest.mark.*") — register it in
markers and add
--strict-markers to fail on typos.
ModuleNotFoundError on import / wrong module shadows another — an --import-mode/layout
issue. For a src/ layout, set pythonpath/importmode=importlib; see
references/configuration.md.
- xfail that passes doesn't fail the suite — use
xfail(strict=True) (or xfail_strict ini)
so an unexpected pass (XPASS) becomes a failure.
- Warnings I want to ignore turn into errors (or vice-versa) — tune
filterwarnings /
-W / @pytest.mark.filterwarnings; see references/cli-usage.md.
Advanced Usage
Comprehensive, source-cited references (each self-contained):
- references/fixtures.md — scopes, autouse, finalization,
request,
parametrized & factory fixtures, conftest sharing, override precedence, usefixtures.
- references/builtin-fixtures.md —
tmp_path/tmp_path_factory,
monkeypatch, capsys/capfd/capteesys, caplog, recwarn, cache, pytestconfig, …
- references/parametrize.md —
parametrize (ids, stacking, indirect),
parametrized fixtures, pytest_generate_tests.
- references/markers-skip-xfail.md — builtin & custom markers,
skip/skipif/xfail, -m expressions, strict markers/config.
- references/assertions.md — assert rewriting,
raises, exception
groups/RaisesGroup, warns/deprecated_call, approx.
- references/cli-usage.md — selection, stopping, cache reruns, output &
traceback flags, durations, debugging, plugin toggles, exit codes.
- references/configuration.md — config files & precedence, rootdir,
ini options,
--import-mode, pythonpath, conftest layering.
- references/plugins-hooks.md — using & writing plugins, the hook
system, key hooks,
pytester.
- references/version-features.md — feature → minimum pytest
version, the bedrock list, and version-check guidance.