pip install pytestNeeds Python 3.10+ — 3.9 was dropped in 9.0.pytest --versionShort form-V. Give it twice to also list plugins.def test_add(): assert add(2,2) == 4★No class, no import, noself.assertEqual. That's a test.pytest★Walks the tree from rootdir and runs everything it finds.python -m pytestSame, but also puts the current directory onsys.path.pytest.main(["-q"])Only way to run pytest from Python.console_main()is deprecated.9.1
pytest tests/Limit to a directory or a single file.pytest test_mod.py::test_fn★Node ID — the exact address of one test.pytest test_mod.py::TestCls::test_fnMethod inside a test class.pytest "test_mod.py::test_fn[3-4]"One parametrized case. Quote it — the shell eats brackets.pytest -k "login and not slow"★Match against test names. Substrings,and/or/not.pytest -m "smoke and not db"★Match against marks. See card 15.pytest -x★Stop at the first failure.--maxfail=3for a budget.pytest --lf★Rerun only last-failed.--ffruns them first, then the rest.pytest --nfNew files first — useful on a big suite you just added to.pytest --swStepwise: stop at a failure, resume there next time.--sw-resetclears it.pytest --ignore=dir --deselect f.py::tDrop a path or one specific test.pytest --pyargs mypkg.testsAddress tests by import path instead of file path.pytest @args.txtRead arguments from a file, one per line.
pytest -v★One line per test.-vvstops truncating diffs;-qis quiet.pytest -s★Don't swallowprint(). Alias for--capture=no.pytest -ra★Summary of everything non-passing.-rAincludes passes.pytest --tb=short★Traceback style:auto long short line native no.pytest -lShow local variables in tracebacks.pytest --durations=10The ten slowest tests. Your first stop when CI drags.pytest --collect-only -q★List what would run, as node IDs. Alias--co.pytest --fixturesEvery fixture visible here, with docstring and scope.pytest --setup-showPrint fixture setup/teardown as it happens.--setup-planis a dry run.pytest --pdbDrop into the debugger on failure.--traceenters at the start.pytest --junit-xml=report.xmlMachine-readable results for CI.pytest --force-short-summaryCondensed failure summary even at high verbosity.
test_*.py or *_test.py★Files. Override withpython_files.def test_*()★Functions, at module level or inside a class.class Test*: # no __init__ !silentA__init__makes the class uncollectable — you get a warning, not an error, and zero tests.rootdir & configfilePrinted in the header. Everything — ini lookup, node IDs — is relative to it.testpaths = ["tests"]★Where to look when you type a barepytest.norecursedirs = ["build", ".venv"]Directories never descended into.collect_imported_tests = falseStop collecting aTest*class merely imported into a test module.8.4pytest --import-mode=importlibModern importing; nosys.pathsurgery, no__init__.pyneeded.pytest a/ a/bOverlapping args now collapse topytest a; duplicates run once. Use--keep-duplicatesfor the old behaviour.9.0
assert got == want★pytest rewrites the bytecode so the failure shows both sides.assert got == want, "context"Custom message is added, not substituted.assert item in collection★Introspection works forin,<,isinstance, dict and set diffs.pytest.register_assert_rewrite("helpers")Rewriting only applies to test modules, conftest and plugins — call this before importing a shared assertion helper.pytest.fail(reason="...")Fail on purpose.pytest.exit()aborts the whole session.assertion_text_diff_style = "left-right"Render string mismatches as separate Left:/Right: blocks instead of an ndiff.9.1def test_x(): return TruefailsReturning anything butNoneis an error — it means you forgot to assert.8.4pytest --assert=plainTurns rewriting off. Your failures become a bareassert False.
with pytest.raises(ValueError):★Passes only if that exception (or a subclass) is raised.pytest.raises(ValueError, match=r"not \d+")★re.search, not a full match — and it's a regex, so escape metacharacters.as exc: ... exc.value.codeexc.valueis the exception itself; also.type,.traceback.pytest.raises(E, check=lambda e: e.errno == 2)Arbitrary predicate on the caught exception.8.4pytest.RaisesGroup(ValueError, KeyError)ForExceptionGroup. Nest it to assert on group structure.8.4with pytest.warns(UserWarning, match="..."):★Same shape asraises, for the warnings module.with pytest.deprecated_call():Shorthand for Deprecation/PendingDeprecation warnings.def test_w(recwarn): recwarn.pop(UserWarning)Fixture form — inspect everything that was warned.pytest.raises(E, match="")An empty pattern matches everything. Usematch="^$"to assert an empty message.
assert 0.1 + 0.2 == pytest.approx(0.3)★The one import worth remembering from the whole assert API.pytest.approx(0.3, rel=1e-3, abs=1e-9)Defaults:rel=1e-6,abs=1e-12. Either one passing is enough.assert [0.1, 0.2] == pytest.approx([0.1, 0.2])Works elementwise on lists, tuples, dicts and numpy arrays.approx(when, abs=timedelta(seconds=2))datetime and timedelta comparisons. An explicitabsis required;relis not supported for datetimes.9.1approx(1) == TrueBooleans compare strictly —Trueis not approximately 1.
@pytest.fixture★Turns a function into something tests can ask for by name.def test_x(wallet): ...★Naming the fixture in the signature is the request. No decorator needed.conn = connect(); yield conn; conn.close()★Everything afteryieldis teardown, and it runs even if the test fails.return valueNo teardown needed? Just return. Both styles are fine.def db(conn, tmp_path): ...★Fixtures request fixtures. pytest resolves the whole graph for you.@pytest.fixture(name="client")Decouple the fixture's name from the function's — handy when both are exported.def make_user(): return _factoryFactory-as-fixture: return a callable when a test needs several objects.request.getfixturevalue("name")Request one dynamically. New requests during teardown are deprecated.9.1value = my_fixture()errorNever call a fixture directly — request it, or extract a plain helper function.
@pytest.fixture(scope="function")★The default: rebuilt fresh for every test.scope="class" | "module" | "session"★Cache the value for a class, a file, or the whole run.scope="package"The forgotten fifth scope — once per directory package. Most cheat sheets omit it.scope=lambda name, config: "session"Dynamic scope: decide at collection time, e.g. from a CLI flag.@pytest.fixture(autouse=True)★Applies to every test in its scope without being requested.@pytest.mark.usefixtures("db", "clean")★Use a fixture for its side effect without taking the argument. Works on classes.@pytest.fixture(params=["pg", "sqlite"])Every test using this fixture runs once per param. Read it viarequest.param.params=[...], ids=["postgres", "sqlite"]Readable names in the test IDs.scope="class" + def f(self)A class-scoped fixture written as an instance method sets attributes on the wrong object. Add@classmethod— deprecated, error in 10.9.1session fixture ← function fixtureA wider scope may never request a narrower one. The reverse is always fine.
tests/conftest.py★Fixtures defined here are visible to every test at or below this directory — with no import.tests/api/conftest.py★Nested files stack. The nearest definition of a name wins.def pytest_collection_modifyitems(items):Hooks live here too — reorder, deselect, auto-mark.def pytest_addoption(parser):Add your own CLI flags. Must be in a top-level conftest.pytest_plugins = ["myplugin"]Only allowed in the rootdir conftest — it loads globally.pytest --noconftestIgnore them all.--confcutdirlimits how far up pytest looks.pytest -p no:conftesterrorconftest files are not plugins; blocking one is now an explicit usage error.9.0
tmp_path★A uniquepathlib.Pathdirectory per test.tmp_path_factorySession-scoped version:.mktemp("data").monkeypatch★Patch attributes, env vars, dicts and cwd — auto-undone.capsys / capfd★Capture stdout+stderr at the Python level / at file-descriptor level.capsysbinary / capfdbinarySame, returningbytes.capteesysCaptures and passes output through to the terminal.8.4caplog★Captureloggingrecords.recwarnRecord all warnings raised in the test.requestThe context object:.param,.node,.config,.addfinalizer.pytestconfigpytestconfig.getoption("--myflag").cachePersist values across runs in.pytest_cache.subtestsMultiple independently-reported checks inside one test.9.0record_property / record_testsuite_propertyAttach key/value data to the JUnit XML.doctest_namespaceInject names into every doctest.pytesterRun pytest inside pytest — for plugin authors. Enable with-p pytester.tmpdir / tmpdir_factoryLegacypy.path.localtwins oftmp_path. Discouraged since 7.x — don't start here.
(tmp_path / "in.txt").write_text("hi")★It's just aPath— no special API to learn.tmp_path_factory.mktemp("shared")One directory for a whole session-scoped fixture.tmp_path_retention_count = "3"pytest keeps the last 3 runs' temp dirs on disk;tmp_path_retention_policyisall|failed|none.monkeypatch.setattr("mod.fetch", fake)★Patch where the name is looked up, not where it's defined.monkeypatch.setattr(obj, "attr", v, raising=False)raising=Falseallows creating an attribute that didn't exist.monkeypatch.setenv("API_KEY", "x")★Anddelenv,setitem,delitem,chdir,syspath_prepend.with pytest.MonkeyPatch.context() as mp:Undo at the end of a block — needed inside session-scoped fixtures.mocker.patch("mod.fn", return_value=1)★Frompytest-mock: unittest.mock with automatic undo and assertion helpers.
out, err = capsys.readouterr()★Reading drains the buffer — the next call returns only what came after.capfd.readouterr()Use when the output comes from C code or a subprocess.caplog.textAlso.records(LogRecord objects) and.messages(formatted strings).caplog.set_level(logging.INFO)★Nothing is captured below the level.caplog.at_level(...)is the block form.log_cli = trueStream logs live during the run; pair withlog_cli_level.logger.propagate = FalseNon-propagating loggers are captured too — previously they vanished. Only loggers that already exist when the test starts, so keepgetLogger()at module level.9.1pytest -s + capsysThe fixture wins: requestingcapsysre-enables capture even under-s.
@pytest.mark.parametrize("n", [1, 2, 3])★One argument: a flat list of values. Three separate tests.("a,b", [(1,2), (3,4)])★Several arguments: a list of tuples, in the same order as the names.pytest.param(6, 36, id="squares")★Name one row. Node ID becomestest_f[squares].pytest.param(..., marks=pytest.mark.xfail)★Mark a single row as expected-to-fail or skipped.ids=["empty", "one"] or ids=fnA callable receives each value and returns its label.stacked decorators★Twoparametrizemarks multiply: 2 × 3 = 6 tests. See diagram 3.indirect=TrueSend the values to a fixture of the same name instead of the test.pytestmark = pytest.mark.parametrize(...)Module-level variable — parametrizes every test in the file.def pytest_generate_tests(metafunc):Build the parameter list programmatically at collection time.strict_parametrization_ids = trueError on duplicate IDs instead of silently appending 0, 1, 2…9.0parametrize("n", (i for i in ...))Generators exhaust after one collection and tests silently disappear. Wrap inlist().9.1
@pytest.mark.slow★Any name you like — it's just a tag.markers = ["slow: takes over a second"]★Register every custom mark in config, or you get a warning per test.strict_markers = true★Turn typo'd marks into errors. The single highest-value setting in this sheet.pytest -m "slow or db"★Boolean expressions over mark names.pytest -m "env(name='staging')"Select on a mark's keyword arguments.8.3pytestmark = [pytest.mark.slow]★Mark every test in the module. On a class, it marks every method.item.add_marker(pytest.mark.slow)Apply marks in bulk frompytest_collection_modifyitems.@pytest.mark.filterwarnings("error")Per-test warning filters; last matching filter wins.@pytest.mark.usefixtures("db")no-op
@pytest.fixtureMarks on a fixture never did anything. Now an error.9.0
@pytest.mark.skip(reason="not built yet")★Never runs. Always give a reason —-rsprints it.@pytest.mark.skipif(sys.platform == "win32", reason=...)★Evaluated at collection time.pytest.skip(reason="no network")★Decide inside the test body. Addallow_module_level=Trueto skip a whole file.pytest.importorskip("numpy")Skip if an optional dependency is absent. Since 9.1 onlyModuleNotFoundErroris caught — a broken install now surfaces.9.1@pytest.mark.xfail(reason="bug #412")★Runs anyway. Fails →x; unexpectedly passes →X.xfail(raises=ValueError)Only that exception counts as the expected failure.xfail(strict=True)★An unexpected pass becomes a failure — so fixed bugs don't stay marked forever.strict_xfail = trueMake that the project default. Renamed fromxfail_strict.9.0xfail(run=False)Don't even execute it — for tests that crash the interpreter.pytest --runxfail / --xfail-tbReport xfails as ordinary results / show their tracebacks.
with subtests.test(path=p):★Loop over cases you only know at runtime; each failure is reported separately instead of stopping at the first.pytest-subtestsis now core.9.0[tool.pytest] # in pyproject.toml★Real TOML types at last — lists are lists, booleans are booleans.9.0pytest.toml / .pytest.tomlA standalone TOML config with a[pytest]table, if you'd rather not touch pyproject.9.0strict = true★One switch forstrict_config,strict_markers,strict_xfailandstrict_parametrization_ids. Pin your pytest version if you enable it.9.0pytest --max-warnings=0Fail the run past a warning budget — exit code 6.9.1pytest.register_fixture(...)Imperative fixture registration, for plugin authors.9.1pytest -p terminalprogressProgress in the terminal tab title. On by default only on Windows — some emulators misbehaved.9.0faulthandler_exit_on_timeout = trueActually kill the process on a deadlock, not just dump threads.9.0PytestRemovedIn9Warningnow errorsThese became errors in 9.0 and the features are gone in 9.1. Upgrade in two steps: land on 9.0, clear the errors, then move to 9.1.
pytest.ini → [pytest]★Highest precedence, and its presence alone sets the rootdir.pyproject.toml → [tool.pytest]★Native TOML.9.0 The older[tool.pytest.ini_options]still works — but you can't have both.tox.ini → [pytest] · setup.cfg → [tool:pytest]Legacy homes, still supported.addopts = ["-ra", "--strict-markers"]★Flags applied to every run. Your team's defaults live here.testpaths = ["tests"]★Makes a barepytestfast in a big repo.filterwarnings = ["error", "ignore::UserWarning"]Promote warnings to failures; later entries win.minversion = "9.0" · required_plugins = [...]Fail fast on a mismatched environment.pythonpath = ["src"]Import your package without installing it. An editable install is still better.pytest -o addopts= -c other.tomlOverride one option ad hoc, or point at a different config file.pytest.ini + pyproject.tomlOnly one file is ever read. Since 9.0 you at least get a warning when several exist.9.0
pytest --cov=mypkg --cov-report=term-missing★pytest-cov7.1 — coverage with the missing lines listed inline.pytest -n auto★pytest-xdist3.8 — one worker per core.--dist=loadgroupkeeps marked tests together.mocker.patch(...)★pytest-mock3.15 —unittest.mockas a fixture, undone automatically.asyncio_mode = "auto"★pytest-asyncio1.4 —autoruns every async test without a decorator. Also setasyncio_default_fixture_loop_scope.@given(st.integers())hypothesis6.x — property-based testing; it generates the awkward inputs you wouldn't.@pytest.mark.django_dbpytest-django4.12 — transactional DB access per test.pytest-timeout · pytest-randomly · pytest-benchmarkKill hangs · shuffle order to expose inter-test dependencies · measure.pytest --disable-plugin-autoloadLoad only what you name with-p. Faster, reproducible CI.8.4pytest -p no:randomlyDisable one plugin for a run.--trace-configshows what's loaded.pip install pytest-subtestsNo longer needed — merged into core in 9.0.
class TestOrder: def test_add(self): ...★Plain classes group related tests. Fixtures work as arguments, as usual.setup_method / teardown_methodxunit-style hooks. Fixtures are more composable, but these are fine for simple cases.setup_class / teardown_classMust be@classmethod. Alsosetup_moduleandsetup_function.class T(unittest.TestCase):Runs unchanged — adopt pytest as a runner first, migrate style later.TestCase + fixture argumentsArgument-style fixtures do not work onunittest.TestCase. Use@pytest.mark.usefixturesor an autouse fixture.pytest --doctest-modulesRun the examples in your docstrings as tests.def setup(self): / def teardown(self):removedThese were nose, not pytest, and stopped running in 8.0 — silently, if you didn't read the notes. Rename tosetup_method.
pytest.warns(None)Removed 8.0. Usepytest.warns()to assert "at least one warning".pytest.skip(msg="...")Removed 8.0 — it'sreason=everywhere now.yield-style testsHard collection error since 8.4. Rewrite asparametrize.async fixture ← sync testAn error since 9.0. Wrap the coroutine in a sync fixture, or use an async plugin.pytest_collect_file(path=...)Thepy.path.localhook parameters were removed in 9.0 — usefile_path,collection_path,start_path.Node(fspath=...)Removed 9.1. Passpath=(apathlib.Path).pytest --pastebinDeprecated 9.1 — moved out to thepytest-pastebinpackage.pytest.yield_fixtureAn alias forpytest.fixturesince 6.2. Safe find-and-replace.config.inicfg["opt"]Deprecated 9.0, removed in 10. Useconfig.getini("opt").
.passedFfailed — an assert in the call phaseEerror — blew up in setup or teardown, not in your testsskippedxxfailed — failed, as expectedXxpassed — passed unexpectedly (a failure understrict)exit 0 / 1all passed / some failedexit 2 / 3 / 4interrupted / internal error / bad usageexit 5no tests collected — CI's favourite silent greenexit 6too many warnings, via--max-warnings9.1