DS DevShelfHub Projects · AI tools
Cheatsheets / pytest
Cheatsheet · Dev tooling

pytest Cheatsheet: Fixtures, Parametrize and Marks Reference

By DevShelfHub

Fixtures, parametrize, marks, mocks, coverage, plugins, asyncio — the Python testing framework as a one-page reference.

112 items 8 min Fixtures Parametrize Marks

Start hereQuick start · 6 you’ll reach for daily

Run allpytest
Name filterpytest -k "login and not slow"
Marker filterpytest -m smoke
Last-failedpytest --lf -x
Parallelpytest -n auto
Coveragepytest --cov=src

Target versions · paceVersions

Targets: pytest ≥ 8 python ≥ 3.9 pytest-cov ≥ 5 pytest-asyncio ≥ 0.23

pytest 8 cleaned up node-id syntax, dropped Python 3.7, and made many warnings hard errors. pyproject.toml is the canonical config home — pytest.ini / setup.cfg still work but get less love. The ecosystem standard is pytest-cov + pytest-xdist + pytest-mock + pytest-asyncio — install them together. This sheet pins to pytest 8+.

Install · runSetup

bash
# Install pytest + commonly-paired plugins
pip install "pytest>=8" pytest-cov pytest-xdist pytest-mock pytest-asyncio pytest-randomly

# Project layout (any of these — pytest auto-discovers)
#   tests/test_*.py
#   tests/*_test.py
#   src//tests/...
#
# Recommended: src/ layout + tests/ at repo root + conftest.py for fixtures.

# Run it
pytest                                  # all tests
pytest tests/test_api.py::test_login    # one test
pytest -k "login and not slow"          # name filter
pytest -m "smoke"                       # marker filter
pytest -x --ff                          # stop on first fail, retry failed-first
pytest -n auto                          # parallel (pytest-xdist)
pytest --cov=src --cov-report=html      # coverage + HTML report
pytest --lf                             # last-failed
pytest -vv --tb=short                   # verbose + concise traceback
pytest --collect-only -q                # show what would run

Where things liveCommon imports

import pytestEntry point for marks, fixtures, raises, warns.
from pytest import fixture, mark, raises, warns, approx, MonkeyPatchConvenience aliases.
from unittest.mock import patch, MagicMock, ANY, callStdlib mocks — pytest-mock’s mocker wraps these.
from pytest_mock import MockerFixtureType hint for the mocker fixture.
import pytest_asyncioAsync fixtures + @pytest_asyncio.fixture.
from pathlib import Path / tmp_path: PathBuilt-in tmp dir fixture.

assert · raises · approxWriting tests

def test_foo(): assert x == 1Bare functions starting with test_. Preferred over class-based.
class TestUser: def test_create(self, db): ...Class-based grouping. No unittest.TestCase needed.
assert actual == expectedPlain assert — pytest rewrites it for rich diffs.
assert {"a": 1} == {"a": 1, "b": 2}Dict / list / set diffs render in the report — no assertEqual ceremony.
with pytest.raises(ValueError, match=r"already \w+"):Expect an exception. match is a regex on str(exc).
excinfo = pytest.raises(Err); excinfo.value / .type / .tracebackInspect after the fact.
with pytest.warns(DeprecationWarning): ...Symmetric to raises for warnings.
assert x == pytest.approx(0.1 + 0.2, rel=1e-9)Float-tolerance comparison. Beats abs(...) < eps.
pytest.fail("explain why") / pytest.skip("...") / pytest.xfail("...")Mark from inside a test based on runtime info.

Setup · teardown · scopeFixtures

@pytest.fixture
def db(): ...
Function-scoped (default). One fresh value per test.
scope="function" | "class" | "module" | "package" | "session"Lifetime. Wider scope = fewer setups, more shared state.
yield valueCode after yield is the teardown. Preferred over finalizer.
@pytest.fixture(autouse=True)Apply to every test in scope without being requested.
@pytest.fixture(params=[1,2,3], ids=["a","b","c"])Run dependent tests once per parameter.
def test_x(db, monkeypatch, tmp_path): ...Inject by parameter name. Order doesn’t matter.
conftest.pyShared fixtures — auto-discovered for tests below the directory.
request.param / request.node / request.clsInside a fixture, read what asked for it.
request.getfixturevalue("name")Resolve a fixture by name at runtime.
@pytest.fixture(name="alias")Expose under a different name (avoid db-shadow problems).
python
# tests/conftest.py — fixtures auto-discovered for every test below this dir
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session

@pytest.fixture(scope="session")
def engine():
    e = create_engine("sqlite:///:memory:")
    yield e
    e.dispose()                                # teardown after the session

@pytest.fixture
def db(engine) -> Session:                     # function-scoped (default)
    with Session(engine) as s:                 # rolled back between tests
        yield s

@pytest.fixture(autouse=True)
def reset_clock(monkeypatch):                  # runs for every test in scope
    monkeypatch.setattr("time.time", lambda: 1_700_000_000.0)

# Parametrize a fixture — every dependent test runs once per value
@pytest.fixture(params=[1, 2, 3], ids=["a", "b", "c"])
def n(request):
    return request.param

# Indirect parametrize — let the fixture transform the value
@pytest.fixture
def user(request):
    return {"plan": request.param}

@pytest.mark.parametrize("user", ["free", "pro"], indirect=True)
def test_billing(user):
    assert user["plan"] in {"free", "pro"}

Ship with pytestBuilt-in fixtures

tmp_path: PathPer-test temp directory. Auto-cleaned.
tmp_path_factorySession-scoped temp dir factory.
capsys / capfdCapture sys / fd stdout+stderr. captured = capsys.readouterr().
caplogCapture logging records. caplog.set_level(...), caplog.records.
monkeypatchSafe patcher: setattr, setenv, delitem, chdir — auto-reverted.
recwarnCapture all warnings raised in the test.
requestIntrospect the test / fixture asking for it.
cacheCross-session key-value store (used by --lf / --ff).
pytestconfigRead CLI / ini config from a test.
doctest_namespaceInject names into doctests.

Table-driven testsParametrize

@pytest.mark.parametrize("a,b,expected", [(1,2,3), (2,2,4)])Cartesian-style table.
ids=["one","two"]Friendly node IDs in output.
pytest.param(1, 2, marks=pytest.mark.xfail, id="known-bug")Per-row marker or id.
@parametrize("x", [1,2])
@parametrize("y", [10,20])
Stacked parametrize = cartesian product.
indirect=TrueSend each param through a fixture transform.
pytest_generate_tests(metafunc)Hook to compute params dynamically (e.g. from a fixtures file).
@parametrize("env", ["dev","prod"], scope="module")Reuse the parametrized value across the module.

skip · xfail · customMarks

@pytest.mark.skip(reason="...")Unconditional skip.
@pytest.mark.skipif(sys.platform == "win32", reason="...")Conditional skip.
@pytest.mark.xfail(reason="known bug", strict=True)Expected failure. strict=True errors if it passes.
@pytest.mark.xfail(raises=ValueError)Only counts as xfail when this exception happens.
@pytest.mark.usefixtures("db", "frozen_time")Apply fixtures without parameters in the signature.
@pytest.mark.smokeCustom marker. Register in [tool.pytest.ini_options].markers.
pytest -m "smoke and not slow"Boolean marker expression on the CLI.
addopts = "--strict-markers"Fail on unregistered markers. Preferred default.
pytest.mark.filterwarnings("error::DeprecationWarning")Per-test warning policy.

pytest-mock · monkeypatchMocking & isolation

mocker.patch("pkg.mod.func", return_value=42)Patch + auto-revert. Preferred over manual with patch(...).
mocker.patch.object(obj, "method", side_effect=Err)Patch on a specific instance / class.
mocker.patch("pkg.mod.fn", side_effect=[1, 2, RuntimeError])Sequence side effects — one per call.
m = mocker.MagicMock(spec=Foo)Spec-bound mock — missing attributes raise.
m.assert_called_once_with(ANY, "x", kw=True)Verify the call. ANY matches anything.
m.call_args_listAll calls, in order.
monkeypatch.setenv("API_KEY", "...")Reverts at test teardown.
monkeypatch.setattr("module.constant", value)Dotted path patch.
monkeypatch.delenv("VAR", raising=False)Remove an env var safely.
freezegun.freeze_time("2026-05-18")Freeze datetime.now() / time.time().

pyproject.toml · addoptsConfig & layout

[tool.pytest.ini_options] in pyproject.tomlPreferred config home. Old pytest.ini still works.
testpaths = ["tests"]Where to look. Speeds up collection.
addopts = "-ra --strict-markers --strict-config"Default CLI args.
pythonpath = ["src"]For src/ layouts — no PYTHONPATH hacks needed.
filterwarnings = ["error", "ignore::Dep:third_party"]Promote warnings to errors; whitelist where needed.
markers = ["slow", "smoke"]Register custom markers (paired with --strict-markers).
norecursedirs = ["build", ".git"]Skip directories during discovery.
conftest.py at each levelLocal fixtures / hooks. Lower-level overrides higher.
pytest_plugins = ["..."]Load plugins from conftest.py (rootdir only).
markdown
# pyproject.toml — the modern home for pytest config
[tool.pytest.ini_options]
minversion       = "8.0"
testpaths        = ["tests"]
addopts          = "-ra -q --strict-markers --strict-config"
python_files     = ["test_*.py", "*_test.py"]
python_classes   = ["Test*"]
python_functions = ["test_*"]
pythonpath       = ["src"]
filterwarnings   = [
    "error",                                  # warnings fail tests by default
    "ignore::DeprecationWarning:third_party",
]
markers = [
    "slow:    deselect with -m 'not slow'",
    "smoke:   minimal subset for PR checks",
    "integration: hits external services",
]

# Coverage (pytest-cov / coverage.py)
[tool.coverage.run]
branch = true
source = ["src"]
omit   = ["*/migrations/*", "tests/*"]

[tool.coverage.report]
show_missing = true
skip_covered = true
fail_under   = 85
exclude_lines = ["pragma: no cover", "raise NotImplementedError"]

pytest-asyncioAsync & concurrent

@pytest.mark.asyncio
async def test_x(): ...
Per-test async opt-in.
asyncio_mode = "auto" (config)Mark every async def test_* automatically.
@pytest_asyncio.fixture
async def client(): ...
Async-aware fixture.
@pytest.fixture(scope="session") def event_loop(): ...Override the loop fixture for shared sessions.
async with httpx.AsyncClient() as c: ...Same idioms as production async code.
pytest -n auto (pytest-xdist)Process-level parallelism. Random order via pytest-randomly.
@pytest.mark.flaky(reruns=3) (pytest-rerunfailures)Retry transient failures — track flakiness, don’t hide it.

Customize collection · reportingHooks & plugins

pytest_addoption(parser)Add a CLI flag — e.g. --env=prod.
pytest_collection_modifyitems(items, config)Tag, skip, or reorder collected tests.
pytest_runtest_makereport(item, call)Hook into pass/fail to attach extra info.
pytest_sessionstart / pytest_sessionfinishSession-level setup / teardown.
pytest_generate_tests(metafunc)Dynamic parametrize at collection time.
@pytest.hookimpl(tryfirst=True)Influence hook ordering between plugins.
conftest.pyWhere hooks live. Plugin via entry point for distributable hooks.
pytest-cov / -xdist / -mock / -asyncio / -randomlyThe everyday plugin set.

Output · coverage · junitReporting & CI

pytest -v / -vv / -qVerbosity. -vv = full diffs.
--tb=short | line | native | noTraceback style.
-x / --maxfail=NStop after N failures.
--lf / --ffLast-failed only / failed-first.
--durations=10Show the 10 slowest tests.
--cov=src --cov-report=term-missing --cov-report=xmlCoverage + missing lines + Cobertura XML.
--cov-fail-under=85Fail the run below threshold.
--junitxml=report.xmlJUnit output for CI test reporters.
PYTEST_ADDOPTS envAppend CLI args from CI without touching code.
pytest --co -qCollect-only — show node IDs without running.

Parametrize · raises · mockerEnd-to-end · user-service tests

A typical test module: parametrized happy path, exception path, mocker for external calls, custom marker for the slow integration test.

python
# tests/test_users.py — parametrize, fixtures, raises, mocker, marker.
import pytest
from unittest.mock import ANY
from myapp.users import create_user, BillingError

@pytest.mark.smoke
@pytest.mark.parametrize("plan,fee", [
    ("free", 0),
    ("pro",  10),
    ("max",  50),
])
def test_create_user(db, plan, fee):
    u = create_user(db, "ana@x.com", plan=plan)
    assert u.id and u.fee == fee

def test_duplicate_email_rejected(db):
    create_user(db, "ana@x.com")
    with pytest.raises(ValueError, match="already exists"):
        create_user(db, "ana@x.com")

def test_billing_error_logs(db, mocker):           # pytest-mock fixture
    log = mocker.patch("myapp.users.log.error")
    mocker.patch("myapp.users.charge", side_effect=BillingError)

    with pytest.raises(BillingError):
        create_user(db, "ana@x.com", plan="pro")

    log.assert_called_once_with("billing failed", user_id=ANY)

@pytest.mark.skipif("os.getenv('CI') is None",
                    reason="needs live Stripe key")
@pytest.mark.integration
def test_stripe_round_trip():
    ...

Best practiceGood to know

Turn on --strict-markers + --strict-config day one. Typos in marker names silently no-op without strict mode — @pytest.mark.smoek would just disappear. Strict mode errors loudly and forces you to register markers in config.
Prefer mocker (pytest-mock) over with patch(...). Same API as unittest.mock but auto-reverted at teardown and composes with other fixtures. Indentation-free tests, fewer leaked patches.
Use tmp_path, never tempfile.mkdtemp(). tmp_path is per-test, auto-cleaned, reported on failures, and works with -n auto. Hand-rolled temp dirs leak when a test crashes.

Common trapsWatch out for

Session-scoped fixtures shared across tests = shared mutable state. One test mutates the value, the next test sees it — usually order-dependent failures only on CI. Default to function scope; widen only when you measure the speedup.
conftest.py location matters. Fixtures only apply to tests below their conftest.py. Defining one inside tests/api/ won’t reach tests/db/. Put truly-shared fixtures at the project root conftest.py.
Don’t patch where the object lives — patch where it’s used. mocker.patch("requests.get") doesn’t affect code that already did from requests import get. Patch myapp.client.get (the consuming module) instead.

Go deeperSee also

pytest FAQ

What is pytest used for?

pytest is the most popular Python testing framework. It discovers and runs test functions automatically, provides rich assertion introspection, and has a plugin ecosystem covering mocking, coverage, asyncio, Django, and more. Its fixture system replaces traditional setUp/tearDown with composable, reusable helpers.

What are pytest fixtures and how do they work?

A fixture is a function decorated with @pytest.fixture that sets up (and optionally tears down) test dependencies. Tests request fixtures by naming them as parameters: def test_user(db_session) triggers the db_session fixture automatically. Fixtures can have function, class, module, or session scope to control how often they run.

What is @pytest.mark.parametrize?

@pytest.mark.parametrize lets you run the same test with multiple input sets without duplicating the function. Decorate with @pytest.mark.parametrize('x,y', [(1,2),(3,4)]) and pytest generates separate test cases for each tuple. Combine multiple parametrize decorators for a cartesian product of inputs.

How do I mock in pytest?

Use the pytest-mock plugin, which provides a mocker fixture wrapping unittest.mock. Call mocker.patch('module.ClassName') to replace an object for the duration of the test — no manual cleanup needed. mocker.patch.object(instance, 'method') patches a specific method. The standard library's unittest.mock.patch also works as a decorator or context manager.

How do I test async code with pytest?

Install pytest-asyncio, add asyncio_mode = 'auto' to pytest.ini (or pyproject.toml), and write test functions with async def. The plugin manages the event loop. Mark specific tests with @pytest.mark.asyncio if not in auto mode. For httpx or aiohttp clients, pytest-httpx or aioresponses provide async-aware mocking.

Is pytest free and open source?

Yes. pytest is MIT-licensed and free to use. It is maintained by the community and is the default testing tool recommended by most Python packaging guides. All major plugins (pytest-mock, pytest-cov, pytest-asyncio) are also free and open source.