pypa/pip · error · ParserSyntaxError

Expected URL after @

Error message

Expected URL after @

What it means

Raised at src/pip/_vendor/packaging/_parser.py:140 inside _parse_requirement_details: once an AT token ('@') is matched and consumed (lines 135-136), the grammar requires a URL token next. tokenizer.expect("URL", expected="URL after @") fails because the URL rule ([^ \t]+, _tokenizer.py:82) cannot match — the position is at end-of-string or at a whitespace-only run. This is the direct-URL requirement syntax (PEP 508 'name @ url') missing its URL.

Source

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

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 d7d0d0a394)

Solutions

  1. Supply a concrete URL after '@', e.g. Requirement('mypkg @ https://example.com/mypkg-1.0.tar.gz').
  2. If you did not mean a direct-URL install, remove the '@' and use a version specifier instead (Requirement('mypkg==1.0')).
  3. If building the string programmatically, assert the URL component is non-empty before concatenating.

Example fix

# before
Requirement('mypkg @')

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

Strategy: validation

Validate before calling

def has_url_after_at(s: str) -> bool:
    if '@' not in s:
        return True  # not a direct-url requirement
    head, _, tail = s.partition('@')
    return bool(tail.strip())  # something non-space must follow '@'

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 'URL after @' in str(e):
        raise ValueError(f'missing URL after @ in {req_str!r}') from e
    raise

Prevention

When it happens

Trigger: Requirement('mypkg @'), Requirement('mypkg @ '), or pip install 'pkg @' where nothing follows the '@'. Also when the '@' was intended as part of a version/extras but the parser interpreted it as the direct-URL introducer.

Common situations: Typing a direct-URL requirement and forgetting the URL, a templated/CI-generated requirement string where the URL variable expanded to empty, or confusing the legacy 'pkg==1.0' form with the 'pkg @ <url>' form.

Related errors


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