pytest cheatsheet.

Basic test

def test_addition():
    assert 1 + 1 == 2

Files: test_*.py or *_test.py. Functions: test_*.

Run

pytest                        # all
pytest tests/                 # specific dir
pytest tests/test_x.py        # specific file
pytest tests/test_x.py::test_one    # specific test
pytest -k "user and not admin"      # by name pattern
pytest -m "slow"              # by marker
pytest -x                     # stop on first failure
pytest --lf                   # last failed
pytest --ff                   # failed first
pytest -v                     # verbose
pytest -s                     # show print output
pytest -p no:cacheprovider    # disable cache

Fixtures

import pytest

@pytest.fixture
def db():
    conn = connect()
    yield conn
    conn.close()

def test_x(db):
    db.execute("...")

Setup + yield + teardown.

Fixture scopes

@pytest.fixture(scope="function")    # default
@pytest.fixture(scope="class")
@pytest.fixture(scope="module")
@pytest.fixture(scope="session")     # for whole test run

Session scope: shared across all tests.

conftest.py

# tests/conftest.py
@pytest.fixture
def client():
    return TestClient(app)

Available to all tests in the directory + subdirs.

Parametrize

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (5, 5, 10),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert a + b == expected

Multiple test cases from one function.

Parametrize with ids

@pytest.mark.parametrize("payload", [
    {"name": "alice"},
    {"name": "bob"},
], ids=["alice-test", "bob-test"])
def test_users(payload):
    ...

Custom test names.

Markers

@pytest.mark.slow
def test_heavy():
    ...

# Run only slow
pytest -m slow

# Run all except slow
pytest -m "not slow"

Register in pyproject.toml:

[tool.pytest.ini_options]
markers = [
    "slow: long-running tests",
    "integration: hits a real DB",
]

Skip / xfail

@pytest.mark.skip(reason="bug #123")
def test_broken(): ...

@pytest.mark.skipif(sys.version_info < (3, 12), reason="3.12+ only")
def test_new_feature(): ...

@pytest.mark.xfail
def test_will_fail():
    assert False
# Marked as expected failure

Raises

import pytest

def test_division_error():
    with pytest.raises(ZeroDivisionError):
        1 / 0

def test_msg():
    with pytest.raises(ValueError, match="must be positive"):
        validate(-1)

pytest.warns

def test_deprecation():
    with pytest.warns(DeprecationWarning):
        old_function()

Capsys / capfd

def test_print(capsys):
    print("hello")
    captured = capsys.readouterr()
    assert "hello" in captured.out

Tmp paths

def test_tmp(tmp_path):
    f = tmp_path / "x.txt"
    f.write_text("hi")
    assert f.read_text() == "hi"

tmp_path is a pathlib.Path; auto-cleaned.

Monkeypatch

def test_env(monkeypatch):
    monkeypatch.setenv("API_KEY", "test")
    monkeypatch.setattr("module.func", lambda: "mocked")
    monkeypatch.delattr("module.attr", raising=False)

Indirect parametrize

@pytest.fixture
def user(request):
    return User(name=request.param)

@pytest.mark.parametrize("user", ["alice", "bob"], indirect=True)
def test_user(user):
    ...

Async tests (pytest-anyio)

import pytest

@pytest.fixture(scope="session")
def anyio_backend():
    return "asyncio"

@pytest.mark.anyio
async def test_async():
    result = await fetch()
    assert result is not None

Or pytest-asyncio:

@pytest.mark.asyncio
async def test_async(): ...

Approx (float comparisons)

import pytest

def test_floats():
    assert 0.1 + 0.2 == pytest.approx(0.3)
    assert [1.0, 2.0] == pytest.approx([1.0, 2.0001], rel=1e-3)

Snapshot testing

# syrupy
def test_snapshot(snapshot):
    result = compute()
    assert result == snapshot

Run once to capture; future runs assert match.

Coverage

pytest --cov=src --cov-report=html --cov-report=term --cov-fail-under=80

Configuration in pyproject.toml

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers --cov=src"
markers = ["slow: long-running"]
filterwarnings = ["error", "ignore::DeprecationWarning"]

Common mistakes

  • Mutable fixture state shared across tests.
  • Forgetting scope="session" for expensive setup.
  • Tests depending on order.
  • time.sleep in tests — flaky; mock time or use polling.
  • Skipping @pytest.mark.parametrize for similar tests.

Read this next

If you want my pytest configuration + plugins setup, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .