pytest-dev/pytest · error · SyntaxError

unexpected reserved python keyword

Error message

unexpected reserved python keyword `{keyword_name.value}`

What it means

A SyntaxError raised in single_kwarg when the kwarg name is a valid identifier but collides with a reserved Python keyword (checked via keyword.iskeyword). Names like `class`, `for`, `lambda`, `return` are syntactically valid identifiers per str.isidentifier yet illegal as kwarg names because they would break the generated AST. The reserved keyword is shown in the message.

Solutions

  1. Rename the kwarg to a non-keyword (e.g. `klass` instead of `class`, `for_` instead of `for`).
  2. If generating names from data, prefix or sanitize them to avoid the keyword set (keyword.kwlist).

Example fix

# before
pytest -m "foo(class='y')"
# after
pytest -m "foo(klass='y')"
Defensive patterns

Strategy: type-guard

Validate before calling

import keyword
def safe_kwarg_name(name: str) -> bool:
    return name.isidentifier() and not keyword.iskeyword(name)

Type guard

import keyword
def is_usable_kwarg_name(name: str) -> bool:
    return isinstance(name, str) and name.isidentifier() and not keyword.iskeyword(name)

Prevention

When it happens

Trigger: Marker expressions like `-m "foo(class='y')"`, `-m "foo(for='y')"`, or `-m "foo(return=1)"` where the name is a Python reserved word. The earlier isidentifier check passes, then this guard fires.

Common situations: Naming marker kwargs after Python keywords, or programmatically generating kwarg names from user data that happens to be a keyword.

Related errors


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

Appendix: source

Thrown at src/_pytest/mark/expression.py:222

        else:
            ret = name
        return ret

    s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))


BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}


def single_kwarg(s: Scanner) -> ast.keyword:
    keyword_name = s.accept(TokenType.IDENT, reject=True)
    if not keyword_name.value.isidentifier():
        raise SyntaxError(
            f"not a valid python identifier {keyword_name.value}",
            (FILE_NAME, 1, keyword_name.pos + 1, s.input),
        )
    if keyword.iskeyword(keyword_name.value):
        raise SyntaxError(
            f"unexpected reserved python keyword `{keyword_name.value}`",
            (FILE_NAME, 1, keyword_name.pos + 1, s.input),
        )
    s.accept(TokenType.EQUAL, reject=True)

    if value_token := s.accept(TokenType.STRING):
        value: str | int | bool | None = value_token.value[1:-1]  # strip quotes
    else:
        value_token = s.accept(TokenType.IDENT, reject=True)
        if (number := value_token.value).isdigit() or (
            number.startswith("-") and number[1:].isdigit()
        ):
            value = int(number)
        elif value_token.value in BUILTIN_MATCHERS:
            value = BUILTIN_MATCHERS[value_token.value]
        else:
            raise SyntaxError(
                f'unexpected character/s "{value_token.value}"',

View on GitHub (pinned to 0d6fbdeffa)