Python Cheatsheet
Testing
Use this Python reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Layout and Discovery
pytest collects by naming convention. Get the names wrong and your tests are
silently not run, which is the most expensive mistake in this file.
| Thing | Must match |
|---|---|
| files | test_*.py or *_test.py |
| functions | test_* at module level |
| classes | Test*, with no __init__ method |
| methods | test_* inside a Test* class |
myproject/
├── pyproject.toml
├── src/myapp/__init__.py
└── tests/
├── conftest.py # shared fixtures, no import needed
├── test_models.py
└── integration/
├── conftest.py # applies only to this subtree
└── test_api.pypython -m pip install pytest python -m pytest # collect from rootdir down python -m pytest tests/ # one directory python -m pytest tests/test_api.py # one file python -m pytest tests/test_api.py::test_login # one test python -m pytest "tests/test_api.py::TestAuth::test_ok" # one method python -m pytest "tests/test_x.py::test_add[2-3]" # one parametrized case python -m pytest --collect-only -q # what WOULD run, run nothing
Use python -m pytest rather than bare pytest, it puts the current directory on
sys.path. Better still, make the package importable with pip install -e . and
keep tests/ outside src/, which is the layout that catches "works only from the
repo root" bugs. Add an __init__.py to tests/ only if two test files share a
basename.
The pytest CLI
| Flag | Effect |
|---|---|
-v · -vv | one line per test · full diffs, no truncation |
-q | quiet, dots only |
-x | stop at the first failure (--exitfirst) |
--maxfail=3 | stop after N failures |
-k "login and not slow" | filter by name expression |
-m "not slow" | filter by marker expression |
--lf | rerun last failed only (--last-failed) |
--ff | failed first, then the rest (--failed-first) |
--nf | new files first (--new-first) |
-s | do not capture stdout, so print and breakpoint() work |
--tb=short · line · native · no | traceback verbosity |
--showlocals (-l) | local variables in the traceback |
--pdb | drop into the debugger on failure |
--durations=10 | the 10 slowest tests |
--durations=10 --durations-min=0.5 | only ones over half a second |
-p no:cacheprovider | disable the .pytest_cache directory |
-p no:randomly | disable a plugin by name |
--strict-markers | error on an unregistered marker instead of warning |
-W error | turn warnings into failures |
-r fEsxX | short summary for failed, error, skipped, xfail, xpass |
--co | short for --collect-only |
-n auto | parallel across cores (needs pytest-xdist) |
--cov=src --cov-report=term-missing | coverage (needs pytest-cov) |
--cov-fail-under=90 | fail the run below a threshold |
python -m pip install pytest-cov pytest-xdist
python -m pytest -x -q --tb=short
python -m pytest -n auto --cov=src --cov-report=term-missing
python -m pytest --lf --tb=long -vv # iterate on what just brokeAssertions
pytest rewrites the bytecode of plain assert statements to report both sides of
the comparison, so there is no assertEqual family to learn.
def test_basics(): assert add(2, 3) == 5 assert "err" not in log assert result == {"id": 1, "tags": ["a"]} # full structural diff on failure assert items, "items should not be empty" # custom message after a comma
import pytest def test_raises(): with pytest.raises(ValueError): int("nope") # match= is a regex run with re.search against str(exception) with pytest.raises(ValueError, match=r"invalid literal"): int("nope") # Inspect the exception afterwards with pytest.raises(ValueError) as exc_info: int("nope") assert "nope" in str(exc_info.value) assert exc_info.type is ValueError # 3.11+ exception groups with pytest.raises(ExceptionGroup) as ei: raise ExceptionGroup("boom", [ValueError("a")]) assert ei.group_contains(ValueError) def test_warns_and_approx(): with pytest.warns(DeprecationWarning, match="use bar"): foo() assert 0.1 + 0.2 == pytest.approx(0.3) # relative 1e-6 default assert 2.0 == pytest.approx(2.0001, abs=1e-3) assert [0.1 + 0.2] == pytest.approx([0.3]) # works on containers assert {"a": 0.3} == pytest.approx({"a": 0.1 + 0.2}) # Each of these ENDS the test immediately, so they are three separate examples pytest.fail("unreachable") # fail on purpose pytest.skip("no network") # skip at runtime, e.g. inside an if pytest.skip("linux only", allow_module_level=True) # skip the whole module pytest.xfail("known bug #412") # mark expected-fail at runtime
Never use a bare assert on floating point equality, and never write assert
x == True. An empty pytest.raises(Exception) catches too much, including a typo
raising NameError, so always narrow the type or add match=.
parametrize
One test function, many cases, each reported separately. This replaces loops inside a test, where the first failure hides the rest.
import pytest @pytest.mark.parametrize("value,expected", [ ("2", 2), ("-7", -7), ("0", 0), ]) def test_parse(value, expected): assert parse(value) == expected # Custom ids, and per-case marks @pytest.mark.parametrize("raw,want", [ pytest.param("1_000", 1000, id="underscores"), pytest.param("0x10", 16, id="hex", marks=pytest.mark.xfail(reason="TODO")), pytest.param("", None, id="empty", marks=pytest.mark.skip), ]) def test_ids(raw, want): assert parse(raw) == want # ids= as a list or a callable @pytest.mark.parametrize("n", [1, 2], ids=["one", "two"]) @pytest.mark.parametrize("op", ["+", "-"]) # STACKED = cartesian product def test_matrix(op, n): # 4 tests: [+-one] [+-two] ... ... # A single argument needs no tuple @pytest.mark.parametrize("bad", ["", " ", "nope", None]) def test_rejects(bad): with pytest.raises(ValueError): parse(bad)
# Parametrize a whole class, or an entire module @pytest.mark.parametrize("backend", ["sqlite", "postgres"]) class TestStore: ... pytestmark = pytest.mark.parametrize("tz", ["UTC", "America/New_York"])
Argument names are a comma-separated string (or a list), and the count must match
each tuple's length exactly. Use pytest.param(..., id=...) whenever the
auto-generated id is unreadable, because that id is how you rerun one case.
Fixtures
A fixture is a named setup function. Request it by putting its name in the test's parameter list, and pytest builds it, injects it, and tears it down.
import pytest @pytest.fixture def user(): return User(name="ada") # return: no teardown @pytest.fixture def db(): conn = connect(":memory:") yield conn # everything after yield is teardown conn.close() # runs even if the test fails @pytest.fixture def api(db, user): # fixtures can request fixtures return Client(db, user) def test_it(api, user): # both injected by name assert api.whoami() == user.name
| Scope | Created once per | Use for |
|---|---|---|
function (default) | each test | anything mutable |
class | each Test* class | shared, read-mostly setup |
module | each test file | a module-wide server or client |
package | each package directory | rare |
session | the whole run | a container, a compiled binary, a schema |
@pytest.fixture(scope="session") def engine(): e = create_engine(URL) yield e e.dispose() @pytest.fixture(autouse=True) # applied to every test in scope def _reset_registry(): REGISTRY.clear() yield REGISTRY.clear() @pytest.fixture(name="client") # rename, e.g. to avoid shadowing def _make_client(): ...
A broader-scoped fixture must never be mutated by a test, and a narrow fixture
cannot be requested by a broader one (session cannot use function). For
expensive setup plus per-test isolation, build once at session scope and roll
back per test in a function-scoped wrapper.
# Parametrized fixture: every test using it runs once per param @pytest.fixture(params=["sqlite", "postgres"], ids=["lite", "pg"]) def store(request): s = make_store(request.param) yield s s.drop() # Indirect parametrization: the TEST supplies the fixture's param @pytest.fixture def user(request): return User(role=request.param) @pytest.mark.parametrize("user", ["admin", "guest"], indirect=True) def test_access(user): assert user.can_read() # Useful request attributes def test_meta(request): request.node.name, request.node.get_closest_marker("slow") request.config.getoption("--verbose") request.addfinalizer(cleanup) # alternative to yield teardown request.getfixturevalue("db") # request a fixture dynamically
conftest.py holds fixtures, hooks, and plugin config for its directory and every
subdirectory. Never import it, pytest loads it automatically, and the nearest
one wins.
# tests/conftest.py import pytest @pytest.fixture(scope="session") def anyio_backend(): return "asyncio" def pytest_addoption(parser): parser.addoption("--slow", action="store_true", help="run slow tests") def pytest_collection_modifyitems(config, items): if config.getoption("--slow"): return skip = pytest.mark.skip(reason="needs --slow") for item in items: if "slow" in item.keywords: item.add_marker(skip)
python -m pytest --fixtures # every available fixture plus its docstring python -m pytest --setup-show # show fixture setup and teardown order
Builtin Fixtures
| Fixture | Gives you |
|---|---|
tmp_path | a fresh pathlib.Path directory, per test |
tmp_path_factory | session-scoped temp dir factory (.mktemp("data")) |
monkeypatch | attribute, env, dict, and cwd patches, auto-undone |
capsys · capsysbinary | captured sys.stdout / sys.stderr |
capfd · capfdbinary | captured file descriptors 1 and 2, so subprocesses too |
caplog | captured logging records |
recwarn | a list of every warning raised |
request | metadata about the requesting test |
pytestconfig | the parsed config and CLI options |
cache | cross-run key/value store |
def test_tmp_path(tmp_path): p = tmp_path / "data.json" p.write_text('{"a": 1}') assert load(p) == {"a": 1} (tmp_path / "sub").mkdir() # a real, writable directory assert p.exists() and p.parent == tmp_path def test_monkeypatch(monkeypatch, tmp_path): monkeypatch.setenv("API_KEY", "test-key") monkeypatch.delenv("HTTP_PROXY", raising=False) monkeypatch.setattr("myapp.http.get", lambda url: {"ok": True}) monkeypatch.setattr(time, "time", lambda: 1700000000.0) monkeypatch.setitem(CONFIG, "debug", True) monkeypatch.delitem(CONFIG, "cache", raising=False) monkeypatch.chdir(tmp_path) monkeypatch.syspath_prepend(str(tmp_path)) # every change is reverted after the test, automatically def test_capsys(capsys): print("hello") out, err = capsys.readouterr() # reading CLEARS the buffer assert out == "hello\n" def test_caplog(caplog): import logging caplog.set_level(logging.WARNING, logger="myapp") do_work() assert "disk full" in caplog.text assert [r.levelname for r in caplog.records] == ["WARNING"] assert caplog.record_tuples == [("myapp", 30, "disk full")]
monkeypatch.setattr with a string target must be an importable dotted path, and
the attribute must already exist unless you pass raising=False. Prefer
monkeypatch over unittest.mock.patch in pytest, it needs no decorator stack and
no nesting.
Markers, Skipping, and xfail
import sys, pytest # A menu, not one stack. Piled on a single test these contradict each other. @pytest.mark.slow # your own marker, register it in config @pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.timeout(5) # needs pytest-timeout def test_thing(): ... @pytest.mark.skip(reason="rewriting the parser") # never runs def test_wip(): ... @pytest.mark.skipif(sys.version_info < (3, 12), reason="needs batched") @pytest.mark.skipif(not os.getenv("DB_URL"), reason="no database") def test_conditional(): ... @pytest.mark.xfail(reason="upstream bug #99") # a pass = XPASS @pytest.mark.xfail(condition=sys.platform == "win32", reason="CRLF") def test_known_broken(): ... @pytest.mark.xfail(raises=TimeoutError, strict=True) # any other outcome FAILS def test_times_out(): ... pytestmark = [pytest.mark.slow] # module level, applies to every test
python -m pytest -m slow python -m pytest -m "not slow" python -m pytest -m "slow and not db" python -m pytest --runxfail # report xfail tests as normal results
Register every custom marker or --strict-markers fails the run, which is what
you want, since an unregistered marker is usually a typo that silently filters
nothing. xfail says "known broken, do not block the build", skip says "not
applicable here". Use strict=True so a fixed bug fails loudly and reminds you to
delete the marker.
Configuration
One config file per project. pyproject.toml if you already have one, otherwise
pytest.ini (which wins over all others when both exist).
# pyproject.toml [tool.pytest.ini_options] minversion = "8.0" testpaths = ["tests"] addopts = "-ra -q --strict-markers --strict-config" pythonpath = ["src"] markers = [ "slow: takes more than a second", "db: needs a live database", ] filterwarnings = [ "error", # warnings are failures "ignore::DeprecationWarning:third_party.*", "default::ResourceWarning", ] log_cli = true log_cli_level = "INFO" xfail_strict = true
; pytest.ini, the same keys in ini form
[pytest]
testpaths = tests
addopts = -ra --strict-markers
markers =
slow: takes more than a secondrootdir is inferred from where the config file lives and it anchors every
relative path, so run pytest from the project root. addopts applies to every
invocation, including CI, so keep it to reporting and strictness flags and never
put -x or -n auto there.
Mocking with unittest.mock
from unittest.mock import Mock, MagicMock, patch, call, ANY, sentinel m = Mock() m.method(1, x=2) # auto-creates any attribute you touch m.method.return_value = 7 # what the call returns m.method.side_effect = ValueError # raise instead m.method.side_effect = [1, 2, 3] # a value per successive call m.method.side_effect = lambda x: x # compute the return value m.prop = 42 # plain attribute Mock(spec=Database) # AttributeError on anything not on Database Mock(return_value=3, name="db") # name= is the repr, set .name separately m.method.assert_called() m.method.assert_called_once() m.method.assert_called_once_with(1, x=2) m.method.assert_any_call(1, x=2) m.method.assert_not_called() m.method.assert_has_calls([call(1), call(2)], any_order=False) m.method.call_count # 2 m.method.call_args # call(1, x=2), the LAST call m.method.call_args.args, m.method.call_args.kwargs m.method.call_args_list # every call, in order m.reset_mock() m.method.assert_called_once_with(ANY, x=2) # ignore one argument's value
MagicMock is a Mock that also implements the dunder protocols, so
len(), in, [], iteration, and context-manager use all work. patch returns
a MagicMock by default.
# patch as a decorator, a context manager, or per-test setup @patch("myapp.service.requests.get") # innermost decorator = FIRST arg @patch("myapp.service.now", return_value=FIXED) def test_fetch(mock_now, mock_get): mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"id": 1} assert fetch(1) == {"id": 1} mock_get.assert_called_once_with("https://api/1", timeout=5) def test_ctx(): with patch("myapp.service.requests.get") as mock_get: mock_get.side_effect = TimeoutError with pytest.raises(ServiceDown): fetch(1) with patch.object(Database, "query", return_value=[]) as q: ... with patch.dict(os.environ, {"ENV": "test"}, clear=True): ... with patch("myapp.CACHE", {}): ... # replace with a real object with patch("myapp.service.get", autospec=True) as g: ...
Patch where it is used, not where it is defined. If
myapp/service.pydoesfrom myapp.http import get, thenserviceholds its own reference, so you must patch"myapp.service.get". Patching"myapp.http.get"has no effect on the nameservicealready bound. If instead the module doesimport myapp.httpand callsmyapp.http.get(...), patch"myapp.http.get".
autospec=True (or create_autospec) builds the mock from the real object's
signature, so a call with the wrong arguments fails immediately. Plain Mock
accepts anything, which means a test can keep passing after you rename a parameter.
Use autospec by default.
from unittest.mock import AsyncMock, create_autospec client = AsyncMock() # 3.8+, awaiting it works client.fetch.return_value = {"ok": True} await client.fetch() # returns the value, not a coroutine client.fetch.assert_awaited_once_with() client.fetch.await_count repo = create_autospec(Repository, instance=True, spec_set=True)
Mock the boundary, not your own code. If a test needs five patches, that is a design signal to inject the dependency instead.
Async Tests
python -m pip install pytest-asyncio # or pytest-anyio / anyioimport asyncio, pytest @pytest.mark.asyncio async def test_fetch(): result = await fetch("/health") assert result["ok"] @pytest.fixture async def client(): # async fixtures work too c = await Client.connect() yield c await c.close() @pytest.mark.asyncio async def test_timeout(): with pytest.raises(TimeoutError): async with asyncio.timeout(0.01): await asyncio.sleep(1)
[tool.pytest.ini_options] asyncio_mode = "auto" # every async test runs, no marker needed
Default mode is strict, so without asyncio_mode = "auto" an unmarked async
def test fails with async def functions are not natively supported, and older
pytest releases skipped it with a warning instead. Either way it never actually
ran, so set the mode explicitly the first time you add async tests.
unittest, Standard Library Only
No install, so this is what you use in a constrained environment or a stdlib-only codebase.
import unittest class TestParse(unittest.TestCase): @classmethod def setUpClass(cls): cls.engine = make_engine() # once per class @classmethod def tearDownClass(cls): cls.engine.close() def setUp(self): self.parser = Parser() # before each test self.addCleanup(self.parser.reset) # LIFO, runs even on error def tearDown(self): pass # after each test def test_int(self): self.assertEqual(self.parser.parse("2"), 2) def test_raises(self): with self.assertRaises(ValueError): self.parser.parse("x") with self.assertRaisesRegex(ValueError, r"invalid"): self.parser.parse("x") @unittest.skip("wip") @unittest.skipIf(sys.platform == "win32", "posix only") @unittest.expectedFailure def test_later(self): ... def test_subtests(self): for raw, want in [("1", 1), ("2", 2)]: with self.subTest(raw=raw): # each case reported separately self.assertEqual(parse(raw), want) if __name__ == "__main__": unittest.main()
| Assertion | Checks |
|---|---|
assertEqual · assertNotEqual | == · != |
assertTrue · assertFalse | truthiness |
assertIs · assertIsNot | identity |
assertIsNone · assertIsNotNone | is None |
assertIn · assertNotIn | membership |
assertIsInstance · assertNotIsInstance | isinstance |
assertAlmostEqual(a, b, places=7) | float comparison |
assertGreater · assertLess · assertGreaterEqual | ordering |
assertRaises · assertRaisesRegex | exceptions |
assertWarns · assertWarnsRegex | warnings |
assertLogs · assertNoLogs (3.10+) | logging output |
assertCountEqual(a, b) | same elements, any order |
assertListEqual · assertDictEqual · assertSetEqual | typed, better diffs |
assertRegex · assertNotRegex | re.search |
IsolatedAsyncioTestCase | async def test_* with asyncSetUp |
python -m unittest # discover from the cwd python -m unittest discover -s tests -p "test_*.py" python -m unittest discover -s tests -t . # -t needs tests/__init__.py python -m unittest -v tests.test_parse python -m unittest tests.test_parse.TestParse.test_int python -m unittest -f # failfast python -m unittest -k login # filter by substring (3.7+) python -m pytest tests/ # pytest RUNS TestCase classes too
pytest collects and runs unittest.TestCase classes, so you can adopt pytest
without rewriting anything. The reverse is not true, pytest fixtures and
parametrize do not work on a TestCase.
Coverage, doctest, Property Tests
python -m pip install coverage coverage run -m pytest # or: pytest --cov=src coverage report -m # terminal table with missing lines coverage report --fail-under=90 coverage html && open htmlcov/index.html coverage xml # for CI dashboards coverage erase
[tool.coverage.run] source = ["src"] branch = true # also measure both sides of a branch omit = ["*/migrations/*", "*/__main__.py"] [tool.coverage.report] show_missing = true skip_covered = true exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError"]
Coverage measures which lines executed, not whether they were checked. A test with no assertions still counts, so treat the number as a floor and never as a goal.
def add(a, b): """Add two numbers. >>> add(2, 3) 5 >>> add(-1, 1) 0 """ return a + b
python -m doctest mymodule.py -v # run the docstring examples python -m pytest --doctest-modules src/ # collect doctests as pytest tests python -m pytest --doctest-glob="*.md" docs/
Doctests keep documentation honest, but they are brittle for anything whose repr is unordered or address-based. Keep them to small, deterministic examples.
python -m pip install hypothesis
from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_sort_is_idempotent(xs): assert sorted(sorted(xs)) == sorted(xs)
hypothesis generates hundreds of inputs, finds the failing one, then shrinks it
to the smallest reproducing case and remembers it. Use it for parsers, encoders,
and anything with a round-trip property, where hand-picked cases miss the edge.