pypa/pip · error · SyntaxError

Invalid URL: %s

Error message

Invalid URL: %s

What it means

Raised by distlib's `parse_requirement` for a direct-URL requirement: after extracting the URI token, the parser checks that the URL has both a scheme and netloc (`urlparse(uri)`); if either is missing the URL is not a valid absolute URL and the parser raises `SyntaxError: Invalid URL`.

Source

Thrown at src/pip/_vendor/distlib/util.py:192

                raise SyntaxError('comma expected in extras: %s' % s)
            s = s[1:].lstrip()
        if not extras:
            extras = None
    if remaining:
        if remaining[0] == '@':
            # it's a URI
            remaining = remaining[1:].lstrip()
            m = NON_SPACE.match(remaining)
            if not m:
                raise SyntaxError('invalid URI: %s' % remaining)
            uri = m.groups()[0]
            t = urlparse(uri)
            # there are issues with Python and URL parsing, so this test
            # is a bit crude. See bpo-20271, bpo-23505. Python doesn't
            # always parse invalid URLs correctly - it should raise
            # exceptions for malformed URLs
            if not (t.scheme and t.netloc):
                raise SyntaxError('Invalid URL: %s' % uri)
            remaining = remaining[m.end():].lstrip()
        else:

            def get_versions(ver_remaining):
                """
                Return a list of operator, version tuples if any are
                specified, else None.
                """
                m = COMPARE_OP.match(ver_remaining)
                versions = None
                if m:
                    versions = []
                    while True:
                        op = m.groups()[0]
                        ver_remaining = ver_remaining[m.end():]
                        m = VERSION_IDENTIFIER.match(ver_remaining)
                        if not m:
                            raise SyntaxError('invalid version: %s' % ver_remaining)

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Include the scheme and host: `mypkg @ https://host/path/file.whl`.
  2. For local files use the `file://` form with absolute path: `mypkg @ file:///abs/path/mypkg.whl`.
  3. Validate the URL with `urllib.parse.urlparse` and confirm both `scheme` and `netloc` are non-empty before committing the requirement.

Example fix

# before
mypkg @ files.pythonhosted.org/packages/.../mypkg-1.0.whl

# after
mypkg @ https://files.pythonhosted.org/packages/.../mypkg-1.0.whl
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

def validate_requirement_url(uri: str) -> None:
    t = urlparse(uri)
    if not (t.scheme and t.netloc):
        raise ValueError(f"Invalid URL (missing scheme/netloc): {uri!r}")

Type guard

from urllib.parse import urlparse
def is_absolute_url(u: str) -> bool:
    t = urlparse(u)
    return bool(t.scheme and t.netloc)

Try / catch

null

Prevention

When it happens

Trigger: Parsing a requirement like `mypkg @ hostname/path` (no scheme) or `mypkg @ file.txt` (no `://` netloc). Triggered when pip parses a direct-URL requirement whose URL is not a properly formed absolute URL.

Common situations: Missing `https://` scheme; using bare paths; copy-paste that lost the scheme; using `file:` URLs without the proper `file:///` form; relative URLs in requirements.

Related errors


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