pypa/pip · error · LegacyDistutilsInstall

uninstall-distutils-installed-package

uninstall-distutils-installed-package

Error message

Cannot uninstall {distribution}

What it means

LegacyDistutilsInstall (InstallationError subclass with code 'uninstall-distutils-installed-package') raised during uninstall when the distribution was installed by plain distutils ('python setup.py install') and pip cannot safely enumerate its files. pip detects this via dist.installed_by_distutils and refuses, pointing the user at manual removal.

Source

Thrown at src/pip/_internal/req/req_uninstall.py:510

                try:
                    namespace_packages = dist.read_text("namespace_packages.txt")
                except FileNotFoundError:
                    namespaces = []
                else:
                    namespaces = namespace_packages.splitlines(keepends=False)
                for top_level_pkg in [
                    p
                    for p in dist.read_text("top_level.txt").splitlines()
                    if p and p not in namespaces
                ]:
                    path = os.path.join(dist_location, top_level_pkg)
                    paths_to_remove.add(path)
                    paths_to_remove.add(f"{path}.py")
                    paths_to_remove.add(f"{path}.pyc")
                    paths_to_remove.add(f"{path}.pyo")

        elif dist.installed_by_distutils:
            raise LegacyDistutilsInstall(distribution=dist)

        elif dist.installed_as_egg:
            # package installed by easy_install
            # We cannot match on dist.egg_name because it can slightly vary
            # i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
            # XXX We use normalized_dist_location because dist_location my contain
            # a trailing / if the distribution is a zipped egg
            # (which is not a directory).
            paths_to_remove.add(normalized_dist_location)
            easy_install_egg = os.path.split(normalized_dist_location)[1]
            easy_install_pth = os.path.join(
                os.path.dirname(normalized_dist_location),
                "easy-install.pth",
            )
            paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)

        elif dist.installed_with_dist_info:
            for path in uninstallation_paths(dist):

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Manually remove the package files and its .egg-info directory (find them with 'python -c "import <pkg>; print(<pkg>.__path__)"').
  2. Reinstall with pip ('pip install --force-reinstall --no-deps <pkg>') to get a RECORD, then 'pip uninstall <pkg>'.
  3. If installed system-wide via OS Python, prefer the OS package manager to remove it.
  4. Stop using 'python setup.py install'; use 'pip install .' for future installs.

Example fix

# before
pip uninstall legacy-pkg   # LegacyDistutilsInstall

# after
pip install --force-reinstall --no-deps legacy-pkg
pip uninstall legacy-pkg
Defensive patterns

Strategy: try-catch

Validate before calling

def installed_by_distutils(dist) -> bool:
    return getattr(dist, 'installed_by_distutils', False)

Type guard

def is_pip_managed(dist) -> bool:
    return not getattr(dist, 'installed_by_distutils', False) and not getattr(dist, 'installed_as_egg', False)

Try / catch

from pip._internal.exceptions import LegacyDistutilsInstall
try:
    run_pip(['uninstall', '-y', 'pkg'])
except LegacyDistutilsInstall:
    print('distutils install; reinstall via pip then uninstall')
    run_pip(['install', '--force-reinstall', '--no-deps', 'pkg'])
    run_pip(['uninstall', '-y', 'pkg'])

Prevention

When it happens

Trigger: 'pip uninstall <pkg>' where <pkg> was installed by distutils (no .egg-info/RECORD usable by pip). The code path lands in the 'elif dist.installed_by_distutils' branch and raises.

Common situations: Legacy packages installed before pip was the norm; system Python with packages installed via 'python setup.py install'; tutorials that still recommend setup.py install.

Related errors


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