pip Error: Cannot Uninstall distutils Package Fix
Use a virtualenv instead of forcing pip past distutils errors.
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
- ✓Python 3 pip basics: installing packages and reading pip error output
- ✓Creating and activating virtualenvs from the terminal
- ✓Basic apt or dnf awareness: OS packages vs project packages
- Fix it now: stop forcing the uninstall, create a venv with
python3 -m venv .venv, and install there. - OS-managed packages belong to apt — pip can't track their files, so it refuses rather than corrupt them.
- sudo pip worsens it by overwriting OS files your boot tools import, breaking apt and cloud-init later.
- PEP 668 markers make pip refuse bare installs; use venvs for projects and pipx for CLI tools.
Think of system Python as a landlord's furnished lobby. The OS placed its own furniture (packages) there and bolted some down without itemized receipts. pip is a mover who refuses to haul away a bolted couch without a receipt, since breaking the lobby breaks every tenant. The fix isn't forcing the mover — it's renting your own room (a virtualenv) and furnishing it freely.
Cannot uninstall a distutils installed project is pip's refusal to remove a package it can't safely track. The error ends with lines like Cannot uninstall 'six'. It is a distutils installed project and thus we cannot accurately determine which files belong to it. Your upgrade stops halfway: the old version stays, the new one never lands, and requirements stay unmet.
It shows up in four everyday spots. An OS-managed package like python3-six was placed by apt, so pip owns no uninstall record. A project install went to system Python instead of a venv, mixing owners in one directory. A past --ignore-installed run left two copies shadowing each other. A modern Debian or Fedora ships PEP 668's EXTERNALLY-MANAGED marker, and pip now refuses the whole operation up front.
The fix is never forcing the uninstall. You'll leave system files alone, create a virtualenv, and install your versions where pip owns everything. This guide shows how to confirm the owner, build the venv, and pick the safe alternative for each case.
OS-Managed Packages: Why pip Refuses Instead of Guessing
OS-managed packages are files your Linux distribution placed and tracks, and pip knows it shouldn't touch them. You'll install python3-six through apt and the files land in /usr/lib/python3/dist-packages with dpkg recording each one. When pip later tries upgrading six, it finds a distutils-era install with no reliable file manifest — old setup.py installs never wrote the RECORD pip needs. Rather than guessing which files are safe, pip aborts with the distutils error. That refusal protects apt's database: deleting a file apt claims breaks future upgrades and can remove modules OS tools import at boot.
Confirm ownership before acting. You'll run apt list --installed | grep six to see the OS claim, and pip show six to see pip's view — a Location inside dist-packages plus thin metadata confirms the split. The fix is leaving those files untouched and installing your version where you own everything. You'll create a venv and run pip there, giving the new six a private directory with full RECORD metadata and clean uninstalls forever.
Treat system site-packages as read-only museum storage. You'll look but never rearrange, and every project gets its own room. Teams that adopt this rule stop seeing the distutils error entirely, since pip inside a venv never meets a file it doesn't own.
Virtualenv Instead: A Private Directory pip Fully Owns
A virtualenv ends this error class by giving pip a directory it fully owns. You'll run python3 -m venv .venv and get a private interpreter plus a private site-packages where every install writes complete metadata. Upgrades there never meet OS files, so the distutils complaint can't fire — there's simply nothing foreign to trip on. Activation with source .venv/bin/activate points pip and python at that directory, and pip -V confirms the path before you install a thing.
Make the venv the only place project packages live. You'll keep a requirements.txt per project, install with pip install -r requirements.txt after activating, and commit the file — never the venv directory. For CLI tools like black or ruff, you'll use pipx instead, which builds one venv per tool and links just the binary. System Python then serves only the OS, exactly as Debian and Fedora intend.
Enforce the habit so future you can't slip. You'll export PIP_REQUIRE_VIRTUALENV=true in your shell profile, making bare pip outside a venv fail with a clear message. That one guard has saved more fleets than any wiki page, since it converts a 52-minute boot outage into a 5-second reminder at install time. Document the four commands in onboarding so every machine matches from day one.
--ignore-installed Risks: Two Copies and Imports That Flip
The --ignore-installed flag bulldozes the guard by installing a second copy alongside the first, and you'll inherit version shadowing. You'll run pip install --ignore-installed six and now two six.py files exist — the OS one in dist-packages and yours in site-packages or user site. Imports resolve by sys.path order, so Monday loads your copy and Tuesday's cron job loads the OS one, producing crashes that vanish when a coworker reproduces them. Debugging by version string lies too, since pip show reports one copy while Python imports the other.
Confirm shadowing with the file path, not the version. You'll run python3 -c "import six; print(six.__file__)" and compare against pip's Location — a mismatch proves two copies. The fix is removing the shadow from user site only, never touching system files, then installing the pinned version once inside the venv. That single owned copy makes imports deterministic across shells, cron, and containers.
Reserve the flag for venv interiors. You'll use it there when a base image pre-seeds a stale wheel, and nowhere else. Code review should treat bare --ignore-installed on system Python like sudo pip: a red flag that fails the build until replaced with a venv install.
PEP 668 Externally-Managed: The Guardrail You Should Keep
PEP 668 made the protection explicit with the EXTERNALLY-MANAGED marker file. You'll find it at /usr/lib/python3.11/EXTERNALLY-MANAGED on Debian 12, Ubuntu 23.04+, and Fedora 38+, carrying a message that this interpreter belongs to the OS. Modern pip reads it and refuses bare installs with an externally-managed-environment error instead of the older distutils wording. Same intent, louder voice: the refusal now arrives before any download, so you waste seconds instead of a half-upgraded system.
Read the marker rather than overriding it. You'll cat the file to confirm the OS claim, then choose the sanctioned path: venv for projects, pipx for apps, apt for system libraries. The --break-system-packages flag exists but amounts to signing a waiver — pip obeys and the corruption risk returns in full. You'll never set it on hosts, images, or shared runners, since one waiver in a Dockerfile propagates to every container built from it.
Standardize on the trio and document it once. You'll write the repo guide as three lines: apt for system, venv for projects, pipx for tools. New hires then never meet either error, and the 52-minute boot outage stays a war story instead of a quarterly event.
Why sudo pip Worsens Everything: Root Can't Fix Ownership
Running sudo pip escalates a blocked upgrade into system damage. You'll type sudo to overpower the refusal, and pip — now root — happily overwrites files in /usr/lib/python3/dist-packages that apt recorded and cloud-init imports. The install reports success, CI passes since containers already built, and weeks later a reboot or an apt upgrade detonates: cloud-init crashes on the bumped six API, apt refuses with checksum mismatches, and 9 fresh hosts fail boot while old ones look healthy. Root didn't fix ownership; it just let pip vandalize with privileges.
Audit for past damage with package eyes. You'll compare pip list against apt list --installed for twin-owned names, and inspect /usr/local/lib versus dist-packages for root-stamped shadows. Recovery means restoring OS files via apt (apt install --reinstall python3-six) and moving project deps into a venv — never deleting system files by hand to settle the difference. Hand-deletion orphans apt records and breaks the next three upgrades instead of one.
Ban the pattern where it breeds. You'll add a CI grep failing on sudo pip and --break-system-packages, and pin base-image system packages through apt only. Teams that enforce this spend their incident budget on real bugs instead of rebuilding hosts that a one-line Dockerfile change bricked.
Safe Upgrade Workflow: venv, Pin, Verify, Repeat
Lock in the safe workflow once and the error never returns. You'll create the venv with python3 -m venv .venv, activate it, upgrade pip inside, and install from a pinned requirements.txt. You'll verify with pip -V showing the .venv path and python -c "import sys; print(sys.prefix)" pointing inside it. Every teammate repeats the same 4 commands, so environments match and the distutils guard never fires — there's no foreign file in the venv to argue over.
Separate the three install kinds for good. You'll reach for apt when the OS needs a library, the venv's pip when the project needs one, and pipx when you need a standalone tool. You'll record each in its own manifest — apt pins in the image file, pip pins in requirements.txt — so upgrades stay scoped. A six bump then touches one venv's lockfile instead of nine hosts' boot path.
Teach the guard as a feature, not friction. You'll tell new hires pip refused in order to save the fleet, and show the venv flow in onboarding. Teams that frame it that way get zero sudo-pip incidents a year, while teams that frame it as annoyance collect waivers until the next 52-minute outage. Rerun pip -V after every activation change, since one stale shell can reinstall into the wrong prefix silently.
which pip && pip -V first. If the path lacks .venv, stop and create one — that 20-second check prevents the entire incident class.sudo pip Upgrade of six Bricked Boot on 9 Fresh Hosts for 52 Minutes
sudo pip install --upgrade six overwriting the OS-owned file.sudo pip install --upgrade six on host boot, overwriting /usr/lib/python3/dist-packages/six.py that apt owned. The pip copy bumped the API past what cloud-init expected, so all 9 newly scaled hosts crashed at boot with an ImportError before the app started. The old hosts kept running on the prior file, which is why only fresh scales failed and the dashboard showed a capacity cliff instead of an app error.python3 -m venv /opt/venv and installs moved there, while the host's python3-six was restored via apt. A CI gate now fails on any sudo pip string, and base images pin python3-six through apt only. The fleet was re-imaged overnight with zero repeat failures.- Never sudo pip on any host the OS itself boots from, since cloud-init and apt share those exact files at startup.
- Isolate app dependencies in /opt/venv inside images, so upgrades touch only files your project owns.
- Grep CI for sudo pip and --break-system-packages strings, because one line in a Dockerfile can ground a whole fleet.
apt list --installed 2>/dev/null | grep -i "six" and pip show six 2>&1 | head -8. Then leave system files alone and run python3 -m venv .venv && source .venv/bin/activate && pip install 'six>=1.16.0'.which pip python3 && pip -V and look for a venv path. If both point at /usr/bin, create one with python3 -m venv .venv && source .venv/bin/activate && which pip and reinstall requirements inside.python3 -c "import six; print(six.__file__, six.__version__)" and ls /usr/lib/python3/dist-packages/ | grep -i six. Remove the user-site shadow only, never system files, then pin one version in the venv.cat /usr/lib/python3*/EXTERNALLY-MANAGED 2>/dev/null || echo 'no marker' and check pip's note with pip install requests 2>&1 | head -5. Then install into a venv or with pipx install <tool> for CLI apps.pip list --format=freeze 2>/dev/null | head -30 and ls -la /usr/local/lib/python3*/dist-packages/ 2>/dev/null | head -10. Rebuild cleanly: new venv, pip install -r requirements.txt, and never sudo pip again.| File | Command / Code | Purpose |
|---|---|---|
| pip_which_python.py | print("exe:", sys.executable) | OS-Managed Packages |
| pip_virtualenv_fix.py | print("system pip:", subprocess.run( | Virtualenv Instead |
| pip_shadow_check.py | print("file:", six.__file__) | --ignore-installed Risks |
| pip_externally_managed.py | from pathlib import Path | PEP 668 Externally-Managed |
Key takeaways
Common mistakes to avoid
5 patternsRunning sudo pip to overpower the uninstall guard
pipx install black for isolated binaries.Using --ignore-installed to bulldoze the guard
Deleting /usr/lib/python3/dist-packages files by hand
pip freeze | grep -i six then pip install 'six>=1.16' in the venv. Keep system packages out of project files.Installing project deps into system Python without a venv
PIP_REQUIRE_VIRTUALENV=true in your shell profile so bare pip outside a venv refuses to run. It turns the mistake into a clear error.Setting --break-system-packages to silence PEP 668
Interview Questions on This Topic
What triggers Cannot uninstall distutils installed project?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
That's Packaging. Mark it forged?
5 min read · try the examples if you haven't