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.
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 # 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) == 4See 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 # 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.
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() # teardownSee 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.
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 == expectedTest 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 ==.
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 toleranceMarkers, 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.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 failurepytest -m slow # run only tests tagged "slow"
pytest -k "raises or warns" # select by name substringSee 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.
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.textSee 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 --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 testsSee pytest-cov. --cov and -n auto need the pytest-cov / pytest-xdist plugins.
Quick Reference
| 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 |
| 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 |
| 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 |
| 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 testsBehavior notes
- Plain
assert, notself.assertEqual. pytest’s assert rewriting is the whole point; you do not subclass anything or learn special assertion methods. tmp_path, not the legacytmpdir.tmp_pathis apathlib.Pathand is the current recommendation;tmpdir(apy.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 withre.search.--covand-n autoare plugins. They come frompytest-covandpytest-xdist, not core pytest; install them first. Register custom markers in config so@pytest.mark.<name>does not warn.
References
pytest documentation (stable)
- Documentation home and Get started
- the failures how-to, fixtures, parametrize
- assertions about exceptions, mark, skip / xfail
- tmp_path, monkeypatch, built-in fixtures reference
Project and plugins