Filter concepts by levelShowing all levels.

Python · pytest

pytest

Discovery, fixtures, parametrization, marks, and async tests — the mechanics that make writing a test fast

  • Plain assertno assertEqual ceremony — pytest rewrites the failure message
  • @pytest.fixturereusable setup, injected by matching a parameter name
  • @parametrizeone test body, many rows of inputs, each its own reported test
  • Markstag tests to select, skip, or expect-fail them

1.Test discovery and assertions

pytest finds tests automatically: files named test_*.py or *_test.py, functions named test_*, classes named Test*. Inside a test, a plain assert is enough — pytest rewrites it to show exactly what values did not match.

Key features

  • pytest collects files matching test_*.py or *_test.py, without being told where they are
  • A function is collected as a test if its name starts with test_
  • A class is collected if its name starts with Test (and it has no __init__)
  • A plain assert expression is enough — no assertEqual/assertTrue/assertIn methods needed
  • On failure, assertion rewriting shows the actual values on both sides of the comparison

Discovery naming rules

Discovery naming rules
ThingNaming rule pytest looks for
Filetest_*.py or *_test.py
Functiontest_*
ClassTest* (no __init__ method)
Method on a Test* classtest_*
From a plain assert to a detailed failure report

File matches test_*.py

discovered automatically, no registration needed

Function matches test_*

collected and run as a test

assert add(2, 2) == 5

ordinary Python — no assertEqual needed

Rewritten at collection time

failure shows: assert 4 == 5, + where 4 = add(2, 2)

  1. File matches test_*.py — discovered automatically, no registration needed
  2. Function matches test_* — collected and run as a test
  3. assert add(2, 2) == 5 — ordinary Python — no assertEqual needed
  4. Rewritten at collection time — failure shows: assert 4 == 5, + where 4 = add(2, 2)

Command

python
def test_something():
    assert actual_value == expected_value

pytest.main()   # or, from a shell: pytest

pytest finds tests by name (test_*.py, test_* functions) — write plain assert statements, and pytest's rewriting shows exactly what did not match on failure.

2.Fixtures and fixture scopes

@pytest.fixture marks a function as reusable setup. Any test that takes a parameter with the same name gets that fixture's return value automatically — pytest injects it, nothing is imported or called directly.

Key features

  • @pytest.fixture marks a function as setup, given to any test naming it as a parameter
  • A test requests a fixture just by naming a parameter with the same name — no import needed
  • yield instead of return splits a fixture into setup (before yield) and teardown (after)
  • scope="function" (default) reruns per test; scope="module" runs once per file
  • A higher-scoped fixture is built once, shared — useful for expensive setup like a DB

Fixture scopes

Fixture scopes
ScopeRebuilt
function (default)Once per test function
classOnce per test class
moduleOnce per test file
sessionOnce for the entire test run
Fixture scopes — how often each rebuilds

function (default)

rebuilt once per test — fully isolated

class

rebuilt once per test class

module

rebuilt once per test file — shared across its tests

session

rebuilt once for the entire test run — cheapest, least isolated

  1. function (default) — rebuilt once per test — fully isolated
  2. class — rebuilt once per test class
  3. module — rebuilt once per test file — shared across its tests
  4. session — rebuilt once for the entire test run — cheapest, least isolated

Command

python
@pytest.fixture
def resource():
    setup_thing = build()
    yield setup_thing   # tests run here
    teardown(setup_thing)   # runs after the test, guaranteed

def test_uses_it(resource):   # parameter NAME matches the fixture
    assert resource.ready

@pytest.fixture supplies setup via parameter-name match; yield splits setup/teardown; scope controls rebuilds.

3.Parametrization

@pytest.mark.parametrize("names", [values]) runs the same test function once per row of values, reporting each as its own separate test — instead of copy-pasting the same test body for every case.

Key features

  • @pytest.mark.parametrize("a,b,expected", [(2,3,5), (0,0,0)]) runs the test once per tuple
  • Each parameter set shows up as its own test ID in the output, like test_add[2-3-5]
  • One failing parameter set fails only that ID — the others still run and report separately
  • Stacking two @parametrize decorators runs the CROSS PRODUCT of both parameter sets

Command

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

@pytest.mark.parametrize("names", [rows]) runs one test body once per row, each reported as its own separately pass/fail test ID.

4.Marks and plugins

A mark like @pytest.mark.slow tags a test so it can be selected, deselected, or skipped by name. A plugin is a package that adds new marks, fixtures, or command-line options — pytest-asyncio and pytest-cov are both plugins.

Key features

  • @pytest.mark.skip(reason="...") always skips a test; skipif(condition) skips conditionally
  • @pytest.mark.xfail marks a test expected to fail — a failure there does not fail the suite
  • A custom mark needs registering in pyproject.toml, or pytest warns it is unknown
  • pytest -m "slow" runs only tests marked slow; pytest -m "not slow" excludes them
  • A plugin is an installed package (pytest-asyncio, pytest-cov) adding marks or fixtures

Built-in marks worth knowing

Built-in marks worth knowing
MarkEffect
@pytest.mark.skip(reason="...")always skips the test, records the reason
@pytest.mark.skipif(cond, reason="...")skips only if cond is True at collection time
@pytest.mark.xfailexpected to fail — a failure does not fail the run; an unexpected PASS is flagged
@pytest.mark.parametrize(...)runs the test once per row of supplied values

Command

python
@pytest.mark.slow
def test_full_batch_import():
    ...

# pytest -m "slow"       -- run only marked tests
# pytest -m "not slow"   -- run everything except marked tests

Marks tag tests for selection (pytest -m) or special handling (skip, xfail); plugins are installed packages that add new marks, fixtures, or CLI options.

5.Async tests, conftest.py, and test configuration

With the pytest-asyncio plugin, an async def test function just runs, awaiting normally inside it. conftest.py holds fixtures shared across every test file in its directory — pytest finds it automatically, no import needed.

Key features

  • pytest-asyncio (a plugin, not built in) lets async def test_x(): functions run and await
  • asyncio_mode = "auto" treats every async def test as an asyncio test automatically
  • conftest.py fixtures are auto-discovered by test files in the same directory or below
  • pyproject.toml's [tool.pytest.ini_options] holds test paths, markers, and plugin options
  • A fixture defined in conftest.py never needs an import in the test file that uses it

Command

python
# conftest.py
@pytest.fixture
def api_client():
    return build_test_client()

# test_users.py -- api_client used with NO import
def test_get_user(api_client):
    assert api_client.get("/users/1").status_code == 200

# an async test, with pytest-asyncio installed and configured
async def test_fetch_user():
    result = await fetch_user(1)
    assert result["id"] == 1

conftest.py fixtures are auto-shared across a directory, no import needed; pytest-asyncio (a plugin) lets async def tests await directly.

Remember

  • pytest finds tests by name (test_*.py, test_* functions) — plain assert is enough, no assertEqual needed.
  • @pytest.fixture supplies setup via parameter-name matching; yield splits it into setup and teardown.
  • @pytest.mark.parametrize runs one test body once per row of values, each reported as its own test ID.
  • conftest.py fixtures are auto-discovered by every test file in the same directory tree, no import needed.

Pro tip

  • Default to function-scoped fixtures; only widen scope (module, session) when setup is genuinely expensive.
  • pytest -m "not slow" runs everything except tests tagged @pytest.mark.slow.
  • pytest-asyncio lets an async def test just await directly — no manual event loop management.
  • An unregistered custom mark produces a warning, not an error — register it to silence the warning.

Next up

  • Mocking

Sources

  • pytest — docs.pytest.org
  • Every example executed with pytest 9.1.1 on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.