pypa/pip · error · ParserSyntaxError
Expected end of dependency specifier
Error message
Expected end of dependency specifier
What it means
Raised by the recursive-descent requirement parser at src/pip/_vendor/packaging/_parser.py:118, where _parse_requirement calls tokenizer.expect("END", ...) after a name, optional extras, URL/version-specifier, and optional marker have all been parsed. "END" is the `$` anchor (src/pip/_vendor/packaging/_tokenizer.py:87), so the error means the parser finished a grammatically complete requirement but the input string still has unconsumed, non-whitespace characters. It surfaces to library users as packaging.requirements.InvalidRequirement (requirements.py:56-57 re-raises ParserSyntaxError).
Source
Thrown at src/pip/_vendor/packaging/_parser.py:118
def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
"""
requirement = WS? IDENTIFIER WS? extras WS? requirement_details
"""
tokenizer.consume("WS")
name_token = tokenizer.expect(
"IDENTIFIER", expected="package name at the start of dependency specifier"
)
name = name_token.text
tokenizer.consume("WS")
extras = _parse_extras(tokenizer)
tokenizer.consume("WS")
url, specifier, marker = _parse_requirement_details(tokenizer)
tokenizer.expect("END", expected="end of dependency specifier")
return ParsedRequirement(name, url, extras, specifier, marker)
def _parse_requirement_details(
tokenizer: Tokenizer,
) -> tuple[str, str, MarkerList | None]:
"""
requirement_details = AT URL (WS requirement_marker?)?
| specifier WS? (requirement_marker)?
"""
specifier = ""
url = ""
marker = None
if tokenizer.check("AT"):
tokenizer.read()View on GitHub (pinned to d7d0d0a394)
Solutions
- Trim the offending requirement string to a single specifier and re-parse; the error's span (e.span) points at the first unconsumed character.
- Split one physical line into multiple requirements (one per line) instead of concatenating with spaces.
- If you intended a marker, put it after a single ';' with one space; if you intended extras, use '[a,b]' immediately after the name.
- Move inline comments to their own line prefixed with '#' (pip strips those; the packaging parser does not).
Example fix
# before
Requirement('requests==2.0 bar')
# after
Requirement('requests==2.0') Defensive patterns
Strategy: try-catch
Validate before calling
import re
from pip._vendor.packaging.requirements import Requirement, InvalidRequirement
_REQ_RE = re.compile(r'^\s*[A-Za-z0-9][A-Za-z0-9._-]*\s*(\[[^\]]*\])?\s*([^;]*)(;.*)?$')
def looks_complete_requirement(s: str) -> bool:
# Cheap pre-check: no obvious trailing junk after the marker/specifier.
return bool(_REQ_RE.match(s)) and not s.rstrip().endswith((',', ')', '(')) Type guard
from typing import TypeGuard
from pip._vendor.packaging.requirements import Requirement, InvalidRequirement
def is_valid_requirement(s: str) -> TypeGuard[str]:
try:
Requirement(s)
except InvalidRequirement:
return False
return True Try / catch
from pip._vendor.packaging.requirements import Requirement, InvalidRequirement
try:
req = Requirement(user_string)
except InvalidRequirement as e:
raise UserFacingError(f"Bad requirement {user_string!r}: {e}") from e Prevention
- Parse requirement strings at config-load time with Requirement(...) and fail fast, never at install time.
- Never concatenate multiple requirements on one line; keep one requirement per line.
- When templating, strip trailing commas/whitespace and assert the result parses before writing it to requirements.txt.
When it happens
Trigger: Calling Requirement("...") (or pip parsing a requirement line) with trailing junk after a complete spec: e.g. 'requests==2.0 bar', 'django>=3.0 ; python_version>="3.8" )' (stray paren), 'foo @ https://x.org/w ; extra_token', or a character no token rule matches (such as '#', '*', or '@' in an unexpected position) left over once the grammar is satisfied.
Common situations: Hand-edited requirements.txt lines that concatenate two requirements on one line, copy-pasted specs that include a trailing comment char without a separator, programmatically-built requirement strings with an accidental extra fragment, or a stray closing bracket/paren left after editing. Also seen when a URL-style requirement has a trailing fragment the grammar does not accept.
Related errors
- Expected URL after @
- Expected extra name after comma
- Expected whitespace after URL
- Expected end of marker expression
- {editable_req} is not a valid editable requirement. It shoul
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/ae603c8e2bfee7e8.json.
Report an issue: GitHub.