pytest Cheatsheet

A visual guide to pytest covering plain-assert tests, reading failures, fixtures, parametrization, testing exceptions and warnings, markers and selection, isolating side effects, and measuring and speeding up the suite.

python
pytest
testing
cheatsheet
Author

James Balamuta

Published

June 26, 2026

pytest is the de-facto standard test runner for Python, and the piece this series has been missing: the rest of the sheets teach you to write pandas, numpy, and scikit-learn code, pytest is how you prove any of it works. A test is just a function named test_* that uses Python’s plain assert, no base class and no special assertion methods, and pytest discovers and runs every test_*.py file for you. The recurring picture in this sheet is the red/green loop pytest prints: collect the test_* nodes, run each one, and report with a status-dot strip (. green pass, F red fail, s/x amber skip/xfail) and a summary bar. When an assert fails, pytest rewrites it to show both sides and a focused diff, so you rarely reach for a debugger. The convention is import pytest in any module that needs pytest.raises, fixtures, or @pytest.mark.*; plain-assert tests need no import at all, which is the point.

Complete pytest cheatsheet (light mode): eight panels covering writing and running your first test, reading the failure, fixtures, parametrization, testing raises and warns, markers and selection, isolating side effects, and measuring and speeding up.

Complete pytest cheatsheet (dark mode): eight panels covering writing and running your first test, reading the failure, fixtures, parametrization, testing raises and warns, markers and selection, isolating side effects, and measuring and speeding up.

Download the full cheatsheet

All eight panels in a single, printable SVG.

Light SVG Dark SVG

Write & Run Your First Test

A pytest test is just a function named test_* that uses Python’s plain assert, no base class and no special assertion methods, and pytest discovers every test_*.py file and test_* function for you when you run pytest. You narrow the run by passing a path or a node id (pytest test_math.py::test_inc), turn the noise down with -q or up with -v, and use --collect-only to see what would run without running anything.

pytest first-test panel: plain assert, run the suite, quiet, run one node, verbose node ids, collect-only.

A test is a function that asserts; pytest finds and runs it.

pytest first-test panel: plain assert, run the suite, quiet, run one node, verbose node ids, collect-only.

A test is a function that asserts; pytest finds and runs it.
pytest                            # discover and run every test
pytest -q                         # quiet: one compact line of results
pytest test_math.py::test_inc     # run a single test by node id
pytest -v                         # verbose: print each node id + outcome
pytest --collect-only             # list what would run, run nothing
# test_math.py  (no `import pytest` needed for plain asserts)
def test_inc():
    assert inc(3) == 4

See Get started.

Read the Failure

When an assert fails, pytest rewrites it so the report shows the actual values on both sides and a focused diff, which is why you rarely need a debugger to understand a failure. Reach for -vv to see the full diff, -l to print local variables in the traceback, --pdb to drop into a debugger at the point of failure, and -x / --maxfail to stop early instead of watching every later test fail.

pytest failures panel: F in the strip, rewritten assert, more diff with -vv, --pdb debugger, locals with -l, stop after first with --maxfail.

Assert rewriting shows you exactly what differed.

pytest failures panel: F in the strip, rewritten assert, more diff with -vv, --pdb debugger, locals with -l, stop after first with --maxfail.

Assert rewriting shows you exactly what differed.
pytest             # a failing test shows .F. and "1 failed, 2 passed"
pytest -vv         # show the full diff between the two sides
pytest -l          # print local variables in the traceback
pytest --pdb       # drop into a debugger at the point of failure
pytest --maxfail=1 # stop after the first failure (alias: -x)

See the failures how-to.

Fixtures & the Dependency Graph

A fixture is a function decorated with @pytest.fixture that returns (or yields) setup, and a test gets it simply by listing the fixture name as a parameter; fixtures can depend on other fixtures, so pytest resolves a small dependency graph before each test. Use yield to add teardown after the test, set scope= (function, class, module, package, session) to control how often a fixture is rebuilt, and put shared fixtures in a conftest.py so sibling test files pick them up automatically with no import.

pytest fixtures panel: define a fixture, request by name, fixtures depend on fixtures, yield setup/teardown, scope, conftest.py.

Fixtures provide setup; tests request them by name.

pytest fixtures panel: define a fixture, request by name, fixtures depend on fixtures, yield setup/teardown, scope, conftest.py.

Fixtures provide setup; tests request them by name.
import pytest

@pytest.fixture
def numbers():
    return [1, 2, 3]

@pytest.fixture
def total(numbers):              # fixtures can depend on fixtures
    return sum(numbers)

def test_total(total):           # request a fixture by naming it
    assert total == 6

@pytest.fixture(scope="module")  # rebuilt once per module, not per test
def db():
    conn = connect()
    yield conn                   # the test runs here
    conn.close()                 # teardown

See the fixtures how-to. Put shared fixtures in conftest.py for automatic discovery.

Parametrize Many Inputs

@pytest.mark.parametrize runs one test body over a table of inputs, and each row becomes its own independent test node with its own id, so a failure points at the exact case that broke. Wrap a row in pytest.param(..., id="name") for a readable id or marks=pytest.mark.xfail to flag a single expected failure, stack the decorator to get the Cartesian product of two argument sets, or use @pytest.fixture(params=[...]) when several tests should all run across the same set of inputs.

pytest parametrize panel: table of cases, generated ids, readable id, xfail a case, stack for a grid, parametrize a fixture.

One test body, many cases; each case is its own node.

pytest parametrize panel: table of cases, generated ids, readable id, xfail a case, stack for a grid, parametrize a fixture.

One test body, many cases; each case is its own node.
import pytest

@pytest.mark.parametrize(
    "x, expected",
    [
        (1, 2),
        pytest.param(5, 6, id="five"),                          # readable id
        pytest.param(0, 1, marks=pytest.mark.xfail(reason="known")),
    ],
)
def test_inc(x, expected):       # one body, three independent nodes
    assert x + 1 == expected

See the parametrize how-to.

Test Raises & Warns

To assert that code fails the way it should, wrap it in with pytest.raises(SomeError):, optionally pinning the message with match= (a regex) or capturing the exception via as excinfo to inspect excinfo.value. The parallel tool for warnings is with pytest.warns(SomeWarning):, and pytest.approx lets you compare floating-point results within a tolerance instead of with exact ==.

pytest raises/warns panel: assert raises, match the message, inspect excinfo, assert warns, pytest.approx, recwarn.

Assert that code raises an exception or emits a warning.

pytest raises/warns panel: assert raises, match the message, inspect excinfo, assert warns, pytest.approx, recwarn.

Assert that code raises an exception or emits a warning.
import pytest

with pytest.raises(ZeroDivisionError):          # assert it raises
    1 / 0

with pytest.raises(ValueError, match=r"invalid"):   # pin the message (regex)
    raise ValueError("invalid input")

with pytest.raises(ValueError) as excinfo:      # capture to inspect
    raise ValueError("boom 42")
assert "42" in str(excinfo.value)

with pytest.warns(UserWarning):                 # assert it warns
    warnings.warn("careful", UserWarning)

assert 0.1 + 0.2 == pytest.approx(0.3)          # float within tolerance

See assertions about exceptions.

Markers, Selection, Skip / xfail

Markers are labels you attach with @pytest.mark.<name> (declare custom ones in config to avoid warnings), and you select by marker with -m or by name substring with -k "expr". Use @pytest.mark.skip / @pytest.mark.skipif to skip tests that should not run here and now, and @pytest.mark.xfail for a known failure you expect, which reports as xfail if it fails and the louder xpass if it unexpectedly passes.

pytest markers panel: tag a marker, run only tagged with -m, select by name with -k, skip, skipif, xfail.

Tag tests, then run the subset you want.

pytest markers panel: tag a marker, run only tagged with -m, select by name with -k, skip, skipif, xfail.

Tag tests, then run the subset you want.
@pytest.mark.slow                # tag a test with a marker
def test_big(): ...

@pytest.mark.skip(reason="not ready")                # skip unconditionally
@pytest.mark.skipif(sys.platform == "win32", reason="posix only")  # conditional
@pytest.mark.xfail(reason="bug #123")                # expect a known failure
pytest -m slow                   # run only tests tagged "slow"
pytest -k "raises or warns"      # select by name substring

See the mark how-to and skip / xfail.

Isolate Side Effects

pytest ships built-in fixtures that give each test a clean world: tmp_path is a fresh, auto-cleaned pathlib.Path directory, and monkeypatch sets env vars and patches attributes or functions and then reverts every change when the test ends. capsys captures stdout/stderr (read it with .readouterr()), caplog captures log records, and the popular pytest-mock plugin adds a mocker fixture whose patches also auto-undo.

pytest isolate panel: tmp_path temp dir, monkeypatch setenv, monkeypatch setattr, capsys, caplog, mocker plugin.

Built-in fixtures hand you a clean temp dir, env, and captured output.

pytest isolate panel: tmp_path temp dir, monkeypatch setenv, monkeypatch setattr, capsys, caplog, mocker plugin.

Built-in fixtures hand you a clean temp dir, env, and captured output.
def test_io(tmp_path):                       # fresh, auto-cleaned dir
    (tmp_path / "f.txt").write_text("hi")

def test_env(monkeypatch):                   # set env, reverted after the test
    monkeypatch.setenv("API_KEY", "x")

def test_now(monkeypatch):                   # patch an attr/function
    monkeypatch.setattr("mod.now", lambda: fixed_dt)

def test_print(capsys):                      # capture stdout/stderr
    print("hi")
    assert capsys.readouterr().out == "hi\n"

def test_log(caplog):                        # capture log records
    assert "boom" in caplog.text

See tmp_path and monkeypatch.

Measure & Speed Up

The pytest-cov plugin adds --cov=yourpackage to report line coverage and --cov-report=term-missing to list the exact uncovered lines, so you can see what the suite never exercises. Tighten the feedback loop with -x (stop on first failure), --lf (re-run only last session’s failures from the cache), -n auto (run in parallel across cores via pytest-xdist), and --durations=N to find the slowest tests worth optimizing.

pytest measure/speed panel: --cov coverage, term-missing, -x stop on first, --lf last failed, -n auto parallel, --durations slowest.

See coverage, fail fast, parallelize, re-run only failures.

pytest measure/speed panel: --cov coverage, term-missing, -x stop on first, --lf last failed, -n auto parallel, --durations slowest.

See coverage, fail fast, parallelize, re-run only failures.
pytest --cov=mypkg                          # line coverage (pytest-cov)
pytest --cov=mypkg --cov-report=term-missing # list the uncovered lines
pytest -x                                   # stop on the first failure
pytest --lf                                 # re-run only last session's failures
pytest -n auto                              # parallel across cores (pytest-xdist)
pytest --durations=10                       # report the 10 slowest tests

See pytest-cov. --cov and -n auto need the pytest-cov / pytest-xdist plugins.

Quick Reference

pytest command line.
Command What it does Area
pytest Discover and run every test Run
pytest -q / -v Quiet one-liner / verbose node ids Run
pytest path::test_name Run a single test by node id Run
pytest --collect-only List tests without running them Run
pytest -k "expr" Select tests by name substring Select
pytest -m marker Select tests by marker Select
pytest -x / --maxfail=N Stop after first / N failures Speed
pytest --lf / --ff Last-failed / failed-first ordering Speed
pytest -l / -vv Locals in traceback / full diff Failures
pytest --pdb Debugger on failure Failures
pytest --cov=pkg Line coverage (pytest-cov) Measure
pytest -n auto Parallel across cores (pytest-xdist) Speed
pytest --durations=10 Report the 10 slowest tests Measure
pytest in-test API (needs import pytest).
Symbol Use
assert expr The assertion (no import, rewritten on failure)
@pytest.fixture Define reusable setup / teardown
@pytest.fixture(scope=...) Reuse per function/class/module/session
@pytest.mark.parametrize(...) One test over many input cases
pytest.param(..., id=, marks=) A labelled / marked single case
pytest.raises(Exc, match=...) Assert an exception is raised
pytest.warns(Warning) Assert a warning is emitted
pytest.approx(value) Float comparison within tolerance
@pytest.mark.skip / skipif Skip a test (always / conditionally)
@pytest.mark.xfail Expect a known failure
Common built-in fixtures (no import).
Fixture Gives you
tmp_path Fresh pathlib.Path temp dir (auto-cleaned)
monkeypatch Set env / patch attrs, auto-reverted
capsys Captured stdout / stderr via .readouterr()
caplog Captured log records (.text, .records)
recwarn List of captured warnings
request Introspect the test / fixture context
pytest result symbols.
Symbol Outcome Color cue
. passed green
F failed (assertion or error) red
E error in a fixture / setup red
s skipped amber
x xfailed (expected failure) amber
X xpassed (unexpected pass) amber / bold

Appendix: Sample Code

A first test file

# test_math.py  (no `import pytest` needed for plain asserts)
def inc(x):
    return x + 1


def test_inc():
    assert inc(3) == 4
$ pytest -q
.                                                                        [100%]
1 passed in 0.01s

Fixtures, including setup/teardown and scope

import pytest


@pytest.fixture
def numbers():
    return [1, 2, 3]


@pytest.fixture
def total(numbers):          # fixtures can depend on fixtures
    return sum(numbers)


def test_total(total):
    assert total == 6


@pytest.fixture(scope="module")
def db():
    conn = {"users": []}     # setup
    yield conn               # the test runs here
    conn.clear()             # teardown, after the last test in the module


def test_db(db):
    db["users"].append("ada")
    assert db["users"] == ["ada"]

Exceptions, warnings, and floats

import warnings
import pytest


def test_raises_with_match():
    with pytest.raises(ValueError, match=r"invalid"):
        raise ValueError("invalid input")


def test_inspect_exception():
    with pytest.raises(ValueError) as excinfo:
        raise ValueError("boom 42")
    assert "42" in str(excinfo.value)


def test_warns():
    with pytest.warns(UserWarning):
        warnings.warn("careful", UserWarning)


def test_approx():
    assert 0.1 + 0.2 == pytest.approx(0.3)

Configure pytest once in pyproject.toml

# pyproject.toml  (the modern, recommended config location)
[tool.pytest.ini_options]
addopts = "-ra -q"
testpaths = ["tests"]
markers = [
    "slow: marks tests as slow (deselect with -m 'not slow')",
]

The tight feedback loop

pytest -x -q              # stop at the first failure
pytest --lf -q           # re-run only last session's failures
pytest -n auto -q        # run in parallel across all cores (pytest-xdist)
pytest --cov=mypkg --cov-report=term-missing   # coverage + missing lines (pytest-cov)
pytest --durations=10    # show the 10 slowest tests

Behavior notes

  • Plain assert, not self.assertEqual. pytest’s assert rewriting is the whole point; you do not subclass anything or learn special assertion methods.
  • tmp_path, not the legacy tmpdir. tmp_path is a pathlib.Path and is the current recommendation; tmpdir (a py.path.local) is legacy.
  • Fixtures resolve a graph. A fixture can request other fixtures as parameters; pytest builds the dependency order before each test and reuses fixtures according to their scope.
  • Each parametrize row is its own node. A failure points at the exact case (e.g. test_inc[3-4]), and you can rerun just that node id.
  • match= is a regex, not a substring. pytest.raises(ValueError, match=r"invalid") searches the exception message with re.search.
  • --cov and -n auto are plugins. They come from pytest-cov and pytest-xdist, not core pytest; install them first. Register custom markers in config so @pytest.mark.<name> does not warn.

References

pytest documentation (stable)

Project and plugins