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.

ThingMust match
filestest_*.py or *_test.py
functionstest_* at module level
classesTest*, with no __init__ method
methodstest_* 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.py
python -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

FlagEffect
-v · -vvone line per test · full diffs, no truncation
-qquiet, dots only
-xstop at the first failure (--exitfirst)
--maxfail=3stop after N failures
-k "login and not slow"filter by name expression
-m "not slow"filter by marker expression
--lfrerun last failed only (--last-failed)
--fffailed first, then the rest (--failed-first)
--nfnew files first (--new-first)
-sdo not capture stdout, so print and breakpoint() work
--tb=short · line · native · notraceback verbosity
--showlocals (-l)local variables in the traceback
--pdbdrop into the debugger on failure
--durations=10the 10 slowest tests
--durations=10 --durations-min=0.5only ones over half a second
-p no:cacheproviderdisable the .pytest_cache directory
-p no:randomlydisable a plugin by name
--strict-markerserror on an unregistered marker instead of warning
-W errorturn warnings into failures
-r fEsxXshort summary for failed, error, skipped, xfail, xpass
--coshort for --collect-only
-n autoparallel across cores (needs pytest-xdist)
--cov=src --cov-report=term-missingcoverage (needs pytest-cov)
--cov-fail-under=90fail 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 broke

Assertions

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
ScopeCreated once perUse for
function (default)each testanything mutable
classeach Test* classshared, read-mostly setup
moduleeach test filea module-wide server or client
packageeach package directoryrare
sessionthe whole runa 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

FixtureGives you
tmp_patha fresh pathlib.Path directory, per test
tmp_path_factorysession-scoped temp dir factory (.mktemp("data"))
monkeypatchattribute, env, dict, and cwd patches, auto-undone
capsys · capsysbinarycaptured sys.stdout / sys.stderr
capfd · capfdbinarycaptured file descriptors 1 and 2, so subprocesses too
caplogcaptured logging records
recwarna list of every warning raised
requestmetadata about the requesting test
pytestconfigthe parsed config and CLI options
cachecross-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 second

rootdir 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.py does from myapp.http import get, then service holds its own reference, so you must patch "myapp.service.get". Patching "myapp.http.get" has no effect on the name service already bound. If instead the module does import myapp.http and calls myapp.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 / anyio
import 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()
AssertionChecks
assertEqual · assertNotEqual== · !=
assertTrue · assertFalsetruthiness
assertIs · assertIsNotidentity
assertIsNone · assertIsNotNoneis None
assertIn · assertNotInmembership
assertIsInstance · assertNotIsInstanceisinstance
assertAlmostEqual(a, b, places=7)float comparison
assertGreater · assertLess · assertGreaterEqualordering
assertRaises · assertRaisesRegexexceptions
assertWarns · assertWarnsRegexwarnings
assertLogs · assertNoLogs (3.10+)logging output
assertCountEqual(a, b)same elements, any order
assertListEqual · assertDictEqual · assertSetEqualtyped, better diffs
assertRegex · assertNotRegexre.search
IsolatedAsyncioTestCaseasync 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.