pytest-dev/pytest · error · SyntaxError

expected ; got

Error message

expected {}; got {}

What it means

A SyntaxError raised by Scanner.reject when the parser expected one token type (or a set of alternatives) but the current token did not match. The message lists the expected token names joined by ' OR ' and the actually-seen token. It is the grammar-level parse error complementing the lexer-level errors 121-123.

Solutions

  1. Read the 'expected X OR Y; got Z' message to see which token is missing.
  2. Complete the expression: add the missing operand, close parenthesis, or remove the trailing operator.
  3. For complex expressions, build them incrementally and re-run to get the next parse hint.

Example fix

# before
pytest -m 'not'
# after
pytest -m 'not slow'
Defensive patterns

Strategy: validation

Validate before calling

from _pytest.mark.expression import Expression
def parse_or_report(expr: str):
    try:
        Expression.compile(expr)
    except SyntaxError as e:
        return {'ok': False, 'msg': e.msg, 'offset': e.offset}
    return {'ok': True}

Try / catch

try:
    deselect_by_mark(items, config)
except SyntaxError as e:
    # 'expected X OR Y; got Z' -> tell user what token is missing
    ...

Prevention

When it happens

Trigger: Malformed grammar such as `-m 'not'` (NOT followed by EOF, expected IDENT/LPAREN), `-m 'a and'` (AND with no right operand), `-m '(a'` (LPAREN never closed, expected RPAREN), or `-m 'foo('` (LPAREN not followed by IDENT for kwargs).

Common situations: Incomplete boolean expressions, dangling operators, unbalanced parentheses, or starting a kwarg list but omitting the identifier.

Related errors


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

Appendix: source

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

    def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...

    @overload
    def accept(
        self, type: TokenType, *, reject: Literal[False] = False
    ) -> Token | None: ...

    def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
        if self.current.type is type:
            token = self.current
            if token.type is not TokenType.EOF:
                self.current = next(self.tokens)
            return token
        if reject:
            self.reject((type,))
        return None

    def reject(self, expected: Sequence[TokenType]) -> NoReturn:
        raise SyntaxError(
            "expected {}; got {}".format(
                " OR ".join(type.value for type in expected),
                self.current.type.value,
            ),
            (FILE_NAME, 1, self.current.pos + 1, self.input),
        )


# True, False and None are legal match expression identifiers,
# but illegal as Python identifiers. To fix this, this prefix
# is added to identifiers in the conversion to Python AST.
IDENT_PREFIX = "$"


def expression(s: Scanner) -> ast.Expression:
    if s.accept(TokenType.EOF):
        ret: ast.expr = ast.Constant(False)
    else:

View on GitHub (pinned to 0d6fbdeffa)