pytest-dev/pytest · error · AttributeError

Marker name must NOT start with underscore

Error message

Marker name must NOT start with underscore

What it means

An AttributeError raised by MarkGenerator.__getattr__ when a user accesses an attribute starting with an underscore, e.g. `pytest.mark._foo`. pytest forbids underscore-prefixed marker names to avoid collisions with internal/dunder attributes and to signal that the name is private. The guard checks `name[0] == '_'` before any marker resolution.

Solutions

  1. Rename the marker without a leading underscore.
  2. If you intended a pytest-internal marker, use the public name instead.
  3. Register custom markers in pytest.ini/markers to get name validation and autocompletion without underscores.

Example fix

# before
@pytest.mark._slow
def test_x(): ...
# after
@pytest.mark.slow
def test_x(): ...
Defensive patterns

Strategy: validation

Validate before calling

def is_public_marker_name(name: str) -> bool:
    return isinstance(name, str) and bool(name) and name[0] != '_'

Type guard

def is_valid_marker_name(name: str) -> bool:
    return isinstance(name, str) and len(name) > 0 and not name.startswith('_')

Prevention

When it happens

Trigger: Writing `@pytest.mark._custom`, `pytest.mark.__something`, or programmatically `getattr(pytest.mark, '_x')`. Also triggered by typos that prepend an underscore or by copy-paste from internal pytest code.

Common situations: Naming a custom marker with a leading underscore for 'private' style, autocompletion inserting a dunder, or iterating attribute names that include private ones.

Related errors


AI-assisted analysis of pytest-dev/pytest@0d6fbdeffa (2026-08-11). Data as JSON: /api/errors/3688cf92b72ddc22. Report an issue: GitHub.

Appendix: source

Thrown at src/_pytest/mark/structures.py:600

    # See TYPE_CHECKING above.
    if TYPE_CHECKING:
        skip: _SkipMarkDecorator
        skipif: _SkipifMarkDecorator
        xfail: _XfailMarkDecorator
        parametrize: _ParametrizeMarkDecorator
        usefixtures: _UsefixturesMarkDecorator
        filterwarnings: _FilterwarningsMarkDecorator

    def __init__(self, *, _ispytest: bool = False) -> None:
        check_ispytest(_ispytest)
        self._config: Config | None = None
        self._markers: set[str] = set()

    def __getattr__(self, name: str) -> MarkDecorator:
        """Generate a new :class:`MarkDecorator` with the given name."""
        if name[0] == "_":
            raise AttributeError("Marker name must NOT start with underscore")

        if self._config is not None:
            # We store a set of markers as a performance optimisation - if a mark
            # name is in the set we definitely know it, but a mark may be known and
            # not in the set.  We therefore start by updating the set!
            if name not in self._markers:
                for line in self._config.getini("markers"):
                    # example lines: "skipif(condition): skip the given test if..."
                    # or "hypothesis: tests which use Hypothesis", so to get the
                    # marker name we split on both `:` and `(`.
                    marker = line.split(":")[0].split("(")[0].strip()
                    self._markers.add(marker)

            # If the name is not in the set of known marks after updating,
            # then it really is time to issue a warning or an error.
            if name not in self._markers:
                # Raise a specific error for common misspellings of "parametrize".
                if name in ["parameterize", "parametrise", "parameterise"]:

View on GitHub (pinned to 0d6fbdeffa)