SuperAPI AI Directory

pytest

Testing & quality

View repository

Updated 10-15 20:32

About this skill

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        # or: uv add --dev pytest   /   uv pip install pytest
pytest --version             # e.g. "pytest 9.1.0"

Confirm the installed pytest version — it governs which features below are available:

pytest --version                                   # human-readable
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".

# test_sample.py
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5          # on failure: "assert 4 == 5  +  where 4 = add(2, 3)"
pytest                 # discover & run everything under the current tree
pytest test_sample.py::test_add   # one test by node id

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()        # setup
    yield conn              # value handed to the test
    conn.close()            # teardown — always runs

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)        # tolerant; works on lists/dicts too

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                      # custom marker — register it (see below)
def test_big_import(): ...

Register custom markers so --strict-markers can catch typos:

# pyproject.toml
[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                         # quieter output
pytest -k "login and not slow"    # select by name substring expression
pytest -m slow                    # select by marker expression
pytest -x                         # stop at first failure
pytest --maxfail=2                # stop after N failures
pytest --lf                       # re-run only last-failed   (--last-failed)
pytest --ff                       # run last-failed first, then the rest
pytest -s                         # don't capture stdout (show prints)
pytest -ra                        # summary of all non-passing outcomes at the end
pytest --pdb                      # drop into the debugger on failure
pytest --durations=10             # report the 10 slowest tests

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:

# pyproject.toml
[tool.pytest.ini_options]            # INI-style table (pyproject.toml support is bedrock)
minversion = "7.0"
addopts = "-ra -q --strict-markers"  # flags applied to every run
testpaths = ["tests"]                # where to look when no path is given
markers = ["slow: long-running tests"]
filterwarnings = ["error"]           # turn warnings into errors

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):