pypa/pip · error · ParserSyntaxError

Expected URL after @

Error message

Expected URL after @

What it means

Raised as a ParserSyntaxError (surfacing as InvalidRequirement) at line 148 when, after consuming the '@' (AT token) for a direct-URL requirement at line 143-144, the next token is not a URL. The grammar at line 135 requires `AT URL (...)`, so anything that is not a valid URL token after '@' triggers this.

Solutions

  1. If you meant a version constraint, replace '@' with a proper operator like '>=' or '==' — '@' is only for direct URL references.
  2. If you meant a direct reference, supply a full URL including scheme, e.g. 'pkg @ https://example.com/pkg-1.0.tar.gz'.
  3. Validate the URL with urllib.parse first and confirm it starts with a known scheme (http/https/file/git+https).

Example fix

// before
Requirement('mypkg @ 2.0')

// after
Requirement('mypkg @ https://example.com/mypkg-2.0.tar.gz')
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse
def looks_like_url_req(s):
    # requirement of form 'name @ url'
    if '@' not in s.split('[')[0]:
        return True  # not a direct-url form
    url = s.split('@', 1)[1].split(';')[0].strip()
    return bool(urlparse(url).scheme)

Try / catch

from packaging.requirements import Requirement, InvalidRequirement
try:
    req = Requirement(raw)
except InvalidRequirement as e:
    if 'URL after @' in str(e):
        # user likely used '@' as a version operator
        ...
    raise

Prevention

When it happens

Trigger: A requirement like 'pkg @' with nothing after, or 'pkg @ not-a-url', or 'pkg @ 2.0' (a version where a URL is expected). The tokenizer's URL rule did not match the token following '@'.

Common situations: Confusing a direct URL reference ('pkg @ https://...') with a version specifier ('pkg >= 2.0'); a typo dropping the URL scheme; copying a requirement and truncating it; using '@' as if it were a version operator.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/49ab93a002014db4. Report an issue: GitHub.

Appendix: source

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

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()
        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")

View on GitHub (pinned to f399c37189)