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

  1. Trim the offending requirement string to a single specifier and re-parse; the error's span (e.span) points at the first unconsumed character.
  2. Split one physical line into multiple requirements (one per line) instead of concatenating with spaces.
  3. If you intended a marker, put it after a single ';' with one space; if you intended extras, use '[a,b]' immediately after the name.
  4. 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

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


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