pypa/pip · error · ParserSyntaxError

Expected whitespace after URL

Error message

Expected whitespace after URL

What it means

Raised at src/pip/_vendor/packaging/_parser.py:144. After a URL is read, if the input is not at END (line 141), the grammar requires whitespace before an optional marker clause: tokenizer.expect("WS", expected="whitespace after URL"). WS only matches ASCII space/tab ([ \t]+, _tokenizer.py:86). It fires when the character immediately after the URL token is neither end-of-input nor an ASCII space/tab.

Source

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

    """
    requirement_details = AT URL (WS requirement_marker?)?
                        | specifier WS? (requirement_marker)?
    """

    specifier = ""
    url = ""
    marker = None

    if tokenizer.check("AT"):
        tokenizer.read()
        tokenizer.consume("WS")

        url_start = tokenizer.position
        url = tokenizer.expect("URL", expected="URL after @").text
        if tokenizer.check("END", peek=True):
            return (url, specifier, marker)

        tokenizer.expect("WS", expected="whitespace after URL")

        # The input might end after whitespace.
        if tokenizer.check("END", peek=True):
            return (url, specifier, marker)

        marker = _parse_requirement_marker(
            tokenizer,
            span_start=url_start,
            expected="semicolon (after URL and whitespace)",
        )
    else:
        specifier_start = tokenizer.position
        specifier = _parse_specifier(tokenizer)
        tokenizer.consume("WS")

        if tokenizer.check("END", peek=True):
            return (url, specifier, marker)

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Insert a single ASCII space between the URL and any trailing marker: 'pkg @ https://x.org/w ; python_version>="3.8"'.
  2. Sanitize the string by replacing non-ASCII whitespace (\u00a0, \u2007, \u202f, \ufeff) with a regular space before parsing.
  3. If no marker is intended, ensure nothing follows the URL.

Example fix

# before
Requirement('mypkg @ https://example.com/x\u00a0;python_version>="3.8"')

# after
Requirement('mypkg @ https://example.com/x ; python_version>="3.8"')
Defensive patterns

Strategy: validation

Validate before calling

import re

def normalize_requirement_ws(s: str) -> str:
    # Replace exotic Unicode spaces with a normal space so the WS rule matches.
    return re.sub(r'[\u00a0\u2007\u202f\ufeff\u2003\u2002]', ' ', s)

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(req_str)
except InvalidRequirement as e:
    if 'whitespace after URL' in str(e):
        req = Requirement(normalize_requirement_ws(req_str))  # retry once
    else:
        raise

Prevention

When it happens

Trigger: A direct-URL requirement where content is glued to the URL with a non-ASCII separator (e.g. a non-breaking space U+00A0 or a form-feed) that the WS rule does not recognize, or a programmatically-constructed string that places a token directly after the URL. Note the URL token ([^ \t]+) is greedy, so in normal prose this branch is rarely reached; when it is, it almost always indicates an unusual/invisible separator character.

Common situations: Requirements copy-pasted from rich-text/word processors that insert non-breaking spaces, CSV/templating output that joins fields without a literal space, or test harnesses feeding crafted bytes.

Related errors


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