pypa/pip · error · ParserSyntaxError

Expected end of marker expression

Error message

Expected end of marker expression

What it means

Raised at src/pip/_vendor/packaging/_parser.py:298 by _parse_full_marker: after _parse_marker returns, tokenizer.expect("END", expected="end of marker expression") demands the input be fully consumed. Because END is the `$` anchor (_tokenizer.py:87), the error means a complete marker expression is followed by leftover tokens — typically a dangling boolean operator, a stray parenthesis, or an orphaned token. It surfaces to library users as packaging.markers.InvalidMarker (markers.py:366-367 re-raises ParserSyntaxError).

Source

Thrown at src/pip/_vendor/packaging/_parser.py:298

        tokenizer.consume("WS")
        if not tokenizer.check("COMMA"):
            break
        parsed_specifiers += tokenizer.read().text
        tokenizer.consume("WS")

    return parsed_specifiers


# --------------------------------------------------------------------------------------
# Recursive descent parser for marker expression
# --------------------------------------------------------------------------------------
def parse_marker(source: str) -> MarkerList:
    return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))


def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
    retval = _parse_marker(tokenizer)
    tokenizer.expect("END", expected="end of marker expression")
    return retval


def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
    """
    marker = marker_atom (BOOLOP marker_atom)+
    """
    expression = [_parse_marker_atom(tokenizer)]
    while tokenizer.check("BOOLOP"):
        token = tokenizer.read()
        expr_right = _parse_marker_atom(tokenizer)
        expression.extend((token.text, expr_right))
    return expression


def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
    """
    marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Complete the boolean expression with a right-hand operand: Marker('python_version >= "3.8" and sys_platform == "linux"').
  2. Balance parentheses — every '(' needs a matching ')'.
  3. Use packaging.markers.Marker(...).serialize() after constructing programmatically so the string is always well-formed.

Example fix

# before
Marker('python_version >= "3.8" and')

# after
Marker('python_version >= "3.8" and sys_platform == "linux"')
Defensive patterns

Strategy: try-catch

Validate before calling

import re

# A complete marker must not end with a boolean operator or an unbalanced paren.
_TRAILING_BOOL = re.compile(r'\b(and|or|not)\s*$', re.IGNORECASE)

def looks_complete_marker(s: str) -> bool:
    return (s.count('(') == s.count(')')
            and not _TRAILING_BOOL.search(s.strip()))

Type guard

from typing import TypeGuard
from pip._vendor.packaging.markers import Marker, InvalidMarker

def is_valid_marker(s: str) -> TypeGuard[str]:
    try:
        Marker(s)
    except InvalidMarker:
        return False
    return True

Try / catch

from pip._vendor.packaging.markers import Marker, InvalidMarker

try:
    m = Marker(marker_str)
except InvalidMarker as e:
    raise UserFacingError(f'Bad marker {marker_str!r}: {e}') from e

Prevention

When it happens

Trigger: Marker('python_version >= "3.8" and') (trailing 'and' with no right operand), Marker('(sys_platform == "linux") )') (extra close paren), Marker('python_version >= "3.8" extra'), or any marker string whose boolean chain is incomplete.

Common situations: Partially-deleted marker expressions in pyproject.toml/setup.cfg, templated markers with an empty variable, or copy-paste that drops the right-hand side of an 'and'/'or'.

Related errors


AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04). Data as JSON: /data/errors/04a64b0fc2b3a91f.json. Report an issue: GitHub.