Skip to content

@pytest.mark.* priority seems wrong #10406

Open

Description

@pmeier

Originally asked about in #10391 and deemed to be a bug by @RonnyPfannschmidt


Suppose I have a test class for which I want to turn warnings into errors. However, some of the parametrizations I'm going to test will emit warnings and I just want to ignore them. I thought, I can write something like

import pytest
import warnings


@pytest.mark.filterwarnings("error")
class TestMarkClass:
    @pytest.mark.parametrize("_", [pytest.param(None, marks=pytest.mark.filterwarnings("ignore"))])
    def test_foo(self, _):
        warnings.warn("foo")

However, it seems that the class wide mark takes priority over the one in the parametrization. On the flip side, if I mark the individual tests like

class TestMarkMethods:
    @pytest.mark.filterwarnings("error")
    @pytest.mark.parametrize("_", [pytest.param(None, marks=pytest.mark.filterwarnings("ignore"))])
    def test_bar(self, _):
        warnings.warn("bar")

    @pytest.mark.parametrize("_", [pytest.param(None, marks=pytest.mark.filterwarnings("ignore"))])
    @pytest.mark.filterwarnings("error")
    def test_baz(self, _):
        warnings.warn("baz")

the mark in the parametrization takes priority. Running everything together with -rA set gives

======================================= FAILURES =======================================
_____________________________ TestMarkClass.test_foo[None] _____________________________
Traceback (most recent call last):
  File "/home/user/main.py", line 9, in test_foo
    warnings.warn("foo")
UserWarning: foo
======================================== PASSES ========================================
=============================== short test summary info ================================
PASSED main.py::TestMarkMethods::test_bar[None]
PASSED main.py::TestMarkMethods::test_baz[None]
FAILED main.py::TestMarkClass::test_foo[None] - UserWarning: foo
============================= 1 failed, 2 passed in 0.68s ==============================

This order seems wrong to me. I would expect the marker in the parametrization takes the highest priority, followed by the marker on the test and lastly the marker on the class.

$ pytest --version
pytest 7.1.3

Activity

  1. added
    type: bugproblem that needs to be addressed
    topic: marksrelated to marks, either the general marks or builtin
    on Oct 21, 2022
  2. david1542 commented on Oct 21, 2022

    @david1542

    I can start working on it, if it's okay. Also, any additional information from the core maintainers would be great :)

  3. RonnyPfannschmidt commented on Oct 21, 2022

    @RonnyPfannschmidt
    Member

    this one relates to the order in which collection parameterize drop the parameterize obtained markers into the marker list
    its a little bit tricky, it would resolve automatically if the FunctionDefinition node was made "real"

  4. david1542 commented on Oct 21, 2022

    @david1542

    @RonnyPfannschmidt Thanks :) Can you shed some more light on this maybe? Specifically, where the markers are merged. I found the get_unpacked_marks and store_mark, but it looks like they operate on the object level 🧐

    Sorry if my questions are too basic, I'm new to this project :)

  5. RonnyPfannschmidt commented on Oct 21, 2022

    @RonnyPfannschmidt
    Member

    the markers are merged in the python function nodes

  6. ojii commented on Aug 13, 2024

    @ojii
    Contributor

    I've run into the same/a similar issue with unexpected marker order. I would expect the order to either be "most test case specific -> least test case specific" or the reverse.

    I've done some experimenting and I found that for function tests, the order is "parametrize level marks" -> "function level marks" -> "pytest_collection_modifyitems in conftest" -> "module level marks". For class tests, the order appears to be "method level marks" -> "parametrize level marks" -> "pytest_collection_modifyitems in conftest" -> "class level marks" -> "module level marks". I'd argue that is confusing.

    Here's the code I used to test this:

    conftest.py

    import pytest
    
    
    def pytest_configure(config):
        config.addinivalue_line("markers", "marker")
    
    
    def pytest_collection_modifyitems(items):
        for item in items:
            item.add_marker(pytest.mark.marker("modifyitems"))

    test_markers.py

    import pytest
    
    pytestmark = [pytest.mark.marker("module")]
    
    
    @pytest.fixture
    def markers(request):
        return [mark.args[0] for mark in request.node.iter_markers(name="marker")]
    
    
    @pytest.mark.marker("function")
    @pytest.mark.parametrize(
        "expected",
        [
            pytest.param(
                ["parametrize", "function", "module", "modifyitems"],
                marks=pytest.mark.marker("parametrize"),
            )
        ],
    )
    def test_markers(expected, markers):
        assert markers == expected
    
    
    @pytest.mark.marker("class")
    class Tests:
        @pytest.mark.marker("method")
        @pytest.mark.parametrize(
            "expected",
            [
                pytest.param(
                    ["parametrize", "method", "class", "module", "modifyitems"],
                    marks=pytest.mark.marker("parametrize"),
                )
            ],
        )
        def test_marker(self, expected, markers):
            assert expected == markers
  7. Colin-b commented on Sep 27, 2024

    @Colin-b

    Coming to this topic after encountering the same issue. Markers defined at test suite level (via modifyitems) should be considered as the first declaration.

    I assume that you most likely import the test file (meaning you register module and class markers) and then execute modifyitems (and that would make sense in order to know the actual items upfront).

    Without changing this behavior that some people might rely upon (that would be weird but you never know), what would you think about introducing a pytestmark at conftest level allowing to actually have marker(s) that would be the actual default for the test suite without the need to call modifyitems, thus resolving the order of markers (test suite -> module -> class -> function).

    Thanks again

  8. RonnyPfannschmidt commented on Jul 24, 2026

    @RonnyPfannschmidt
    Member

    Note

    AI-assisted analysis (driven and reviewed by @RonnyPfannschmidt). Reproduced against current main.

    Still reproduces

    The original example still fails on main: TestMarkClass::test_foo fails (class-level filterwarnings("error") wins over the parametrize filterwarnings("ignore")), while both TestMarkMethods variants pass. I also checked it against #14630 (the MRO marker-ordering branch) — identical result, that PR does not address this issue (it fixes a different axis, see below).

    Root cause: two ordering inversions that cancel in one case but not the other

    _pytest/warnings.py applies the marks like this:

    for mark in item.iter_markers(name="filterwarnings"):
        for arg in mark.args:
            warnings.filterwarnings(*parse_warning_filter(arg, escape=False))

    warnings.filterwarnings() inserts at the front of the filter list, so the last-applied filter wins. That means to make the closest mark win, this loop would need to iterate farthest-first. It iterates closest-first, so the farthest mark wins.

    Printing the actual iter_markers("filterwarnings") order (closest → farthest):

    case iter_markers order last-applied (wins) outcome
    function (test_bar) error(decorator), ignore(param) ignore passes — right answer, wrong reason
    class (test_foo) ignore(param), error(class) error fails

    So there are two separate bugs:

    1. warnings.py consumes markers in the wrong direction — with front-insertion it should apply farthest-first so the closest mark ends up winning.
    2. iter_markers yields the parametrize/callspec mark after the function-decorator mark for a Function node (python.py extends own_markers with the decorator marks first, then callspec.marks), i.e. the param mark comes out as farthest rather than closest.

    In the function case these two inversions cancel out, so it happens to pass. In the class case iter_markers is already correct (param first), nothing cancels, and the bug surfaces.

    Implication for a fix

    Fixing only warnings.py (reverse the consumption order) repairs the class case but breaks the function case, because iter_markers has that pair backwards. A correct fix needs both:

    1. warnings.py: apply filters farthest-first (e.g. reversed(list(item.iter_markers(...)))).
    2. Function.iter_markers: yield the parametrize/callspec mark before the function-decorator mark.

    Step 2 is the sensitive one — own_markers order also influences parametrize id/naming, so the reordering likely belongs in the consumer (or in a closest-first iteration helper) rather than in own_markers itself. #14630 recently introduced a _iter_own_markers_closest_first() hook for exactly this kind of closest-first correction on Class nodes; a full fix here would extend that idea to the Function/callspec axis and pair it with the warnings.py change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: marksrelated to marks, either the general marks or builtintype: bugproblem that needs to be addressed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions