When are `pytest_runtest_teardown` and `pytest_runtest_setup` called for skipped tests?

I have a plugin with these hooks:

def pytest_runtest_setup(item):
    item.config.bla = Bla()

def pytest_runtest_teardown(item):
    item.config.bla.do_bla()
    item.config.bla = None

For tests marked as skipped:

@pytest.mark.skip(reason='bla bla')
def test_bla():
    pass

I noticed pytest_runtest_setup is not called, but pytest_runtest_teardown is called, causing an AttributeError because item.config.bla is None.

Does it make sense that setup is skipped but teardown runs for skipped tests, and how can I handle this properly?

From my experience, pytest’s lifecycle can be a bit counterintuitive: skipped tests skip the setup phase but still run teardown to ensure any global resources or fixtures get a chance to clean up.

In your plugin, I usually wrap teardown code in a guard to avoid AttributeError:

def pytest_runtest_teardown(item):
    bla = getattr(item.config, "bla", None)
    if bla:
        bla.do_bla()
        item.config.bla = None

This way, skipped tests don’t break your plugin, but your teardown still works for normal tests. It’s a small pattern I always include when I manipulate config or custom objects.

I ran into this exact behavior while writing a pytest plugin. When a test is skipped, pytest never calls pytest_runtest_setup because the test is never set up, but it still calls pytest_runtest_teardown.

This is intentional, so hooks always get a chance to clean up any lingering state.

In your case, item.config.bla doesn’t exist if the test was skipped. A simple fix is to check if it’s set before calling do_bla():


def pytest_runtest_teardown(item):
    if hasattr(item.config, "bla") and item.config.bla is not None:
        item.config.bla.do_bla()
        item.config.bla = None

I’ve used this pattern in my plugins, and it prevents skipped tests from causing teardown errors.

I faced a similar issue when adding custom hooks to handle per-test state. It’s expected: setup won’t run for skipped tests, but teardown will.

That’s because pytest guarantees teardown hooks always run, even if setup fails or the test is skipped.

You can handle this safely by checking the attribute exists before using it:

def pytest_runtest_teardown(item):
    try:
        item.config.bla.do_bla()
    except AttributeError:
        pass  # test was skipped, nothing to clean up

I prefer getattr or a try/except here because it makes your plugin resilient to skipped or failed setup, and avoids spurious errors.