pypa/pip · error · ValueError

Sorry, {url!r} is a malformed VCS url. The format is <vcs>+<

Error message

Sorry, {url!r} is a malformed VCS url. The format is <vcs>+<protocol>://<url>, e.g. svn+http://myrepo/svn/MyApp#egg=MyApp

What it means

Raised by `VCSTool.get_url_rev_and_auth` when a URL passed to pip as a VCS requirement has no `+` in its URL scheme. pip requires the explicit `<vcs>+<protocol>://` form (e.g. `svn+https://`, `git+ssh://`) so it can identify which backend to dispatch to. A bare `https://...` or `svn://...` scheme is ambiguous and is rejected before any network operation.

Source

Thrown at src/pip/_internal/vcs/versioncontrol.py:383

        information can be provided via the --username and --password options
        instead of through the URL.  For other subclasses like Git without
        such an option, auth information must stay in the URL.

        Returns: (netloc, (username, password)).
        """
        return netloc, (None, None)

    @classmethod
    def get_url_rev_and_auth(cls, url: str) -> tuple[str, str | None, AuthInfo]:
        """
        Parse the repository URL to use, and return the URL, revision,
        and auth info to use.

        Returns: (url, rev, (username, password)).
        """
        scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
        if "+" not in scheme:
            raise ValueError(
                f"Sorry, {url!r} is a malformed VCS url. "
                "The format is <vcs>+<protocol>://<url>, "
                "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp"
            )
        # Remove the vcs prefix.
        scheme = scheme.split("+", 1)[1]
        netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme)
        rev = None
        if "@" in path:
            path, rev = path.rsplit("@", 1)
            if not rev:
                raise InstallationError(
                    f"The URL {url!r} has an empty revision (after @) "
                    "which is not supported. Include a revision after @ "
                    "or remove @ from the URL."
                )
            rev = urllib.parse.unquote(rev)
        url = urllib.parse.urlunsplit((scheme, netloc, path, query, ""))

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Rewrite the URL to the `<vcs>+<protocol>://<host>/<path>` form, e.g. `git+https://github.com/org/repo.git`.
  2. Add the VCS prefix matching the protocol you actually use: `git+ssh://`, `svn+https://`, `hg+https://`, `bzr+http://`.
  3. If installing from a plain tarball/wheel, don't route it through VCS handling — give pip the direct archive URL instead.

Example fix

# before
pip install svn://myrepo/svn/MyApp

# after
pip install svn+https://myrepo/svn/MyApp#egg=MyApp
Defensive patterns

Strategy: validation

Validate before calling

import urllib.parse

def is_valid_vcs_url(url: str) -> bool:
    scheme = urllib.parse.urlsplit(url).scheme
    return "+" in scheme and scheme.split("+", 1)[0] in {"git", "svn", "hg", "bzr"}

# before pip install:
assert is_valid_vcs_url(req_url), f"{req_url!r} is not a valid VCS URL"

Type guard

def is_vcs_url(url: str) -> bool:
    s = urllib.parse.urlsplit(url).scheme
    return bool(s) and "+" in s

Try / catch

null

Prevention

When it happens

Trigger: Pip resolves a requirement link whose `urlsplit(url).scheme` contains no `+` character — e.g. `svn://host/repo`, `git://host/repo`, or a plain `https://github.com/...` URL routed through the VCS path. Triggered during `get_url_rev_and_auth(url)` at the `if '+' not in scheme:` check.

Common situations: Copying a repo browse URL from a browser instead of the VCS install form; misconfiguring `requirements.txt` or `dependency_links`; forgetting the `git+`/`svn+`/`hg+`/`bzr+` prefix; using `svn://` or `git://` protocols directly which lack a `+<protocol>` separator.

Related errors


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