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
- Complete the boolean expression with a right-hand operand: Marker('python_version >= "3.8" and sys_platform == "linux"').
- Balance parentheses — every '(' needs a matching ')'.
- 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
- Construct markers programmatically and call Marker(...).serialize() rather than hand-writing strings.
- Before saving a marker, balance '(' and ')' and ensure it does not end with 'and'/'or'/'not'.
- Lint pyproject.toml/setup.cfg markers with is_valid_marker in CI.
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
- Expected whitespace after 'not'
- invalid version: %s
- invalid constraint: %s
- invalid requirement: %s
- unexpected trailing data: %s
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/04a64b0fc2b3a91f.json.
Report an issue: GitHub.