pip: error: externally-managed-environment
Hi, it's BlueByte. You run pip3 install requests on a fresh Ubuntu 24.04 or Debian 12 host and, instead of a download, pip prints error: externally-managed-environment and a paragraph about apt, venv, and pipx. Nothing is broken and pip hasn't lost its mind: your distribution marked its Python as owned by the OS package manager, and pip is honouring that mark as PEP 668 asks it to. We'll walk through where the message comes from, why sudo and --user don't get past it, the three supported ways to install, when the override is defensible, and how to confirm you ended up in the right interpreter.
What the error looks like and who wrote each line
Here is the full output on Ubuntu 24.04 with Python 3.12:
$ pip3 install requests
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.
If you wish to install a non-Debian packaged Python application,
it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.
See /usr/share/doc/python3.12/README.venv for more information.
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.Only the first line, the note:, and the hint: belong to pip. The indented paragraph is copied out of a file your distribution ships, which is why the wording differs between Debian, Fedora, Arch, and Homebrew — each writes its own. sudo pip3 install and pip3 install --user print the same thing, and pip 23.0 was the first release to honour the marker, so a host that upgraded from an older distro release starts refusing overnight.
The marker file that switches the refusal on
PEP 668 defines one file, EXTERNALLY-MANAGED, in the interpreter's standard-library directory. pip refuses when two conditions hold at once: the file exists, and you are not inside a virtual environment (sys.prefix == sys.base_prefix). If you check it yourself:
STDLIB=$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')
cat "$STDLIB/EXTERNALLY-MANAGED"[externally-managed]
Error=To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
...It's a plain INI file whose Error key is the paragraph pip printed. On Ubuntu it belongs to libpython3.12-stdlib, so deleting it only lasts until the next Python security update reinstalls it. Debian's release notes explain the intent: pip uninstall or an upgrade of a library that apt also ships can remove files apt owns, and on Ubuntu the tools that run on that interpreter include unattended-upgrade and cloud-init. That is what the refusal protects.
Why sudo and --user don't help
Both paths land packages on the system interpreter's sys.path — /usr/lib/python3/dist-packages for root, ~/.local/lib/python3.12/site-packages for --user — and both shadow or collide with apt-managed modules the same way, so PEP 668 blocks both on purpose. There is no permission problem to fix here; the interpreter is the wrong target. Everything below is about picking the right one.
Fix for a project dependency: a virtual environment
A venv has its own sys.prefix, so pip inside it never consults the marker:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requestsInstalling collected packages: urllib3, idna, charset_normalizer, certifi, requests
Successfully installed certifi-2026.7.22 charset_normalizer-3.5.1 idna-3.19 requests-2.34.2 urllib3-2.8.0You don't have to activate anything — .venv/bin/pip install requests and .venv/bin/python app.py work by path, which is what you want in cron jobs and systemd units. If python3 -m venv fails saying ensurepip is not available, the venv module isn't installed yet; sudo apt install python3-venv (or python3-full, as the message suggests) fixes that.
Tools get pipx, scripts the OS runs get apt
For things you run rather than import — black, ansible, httpie — pipx builds one venv per application and puts the entry point on your PATH:
sudo apt install pipx
pipx ensurepath
pipx install blackpipx reports the package it installed and the apps it exposed. Open a new shell after ensurepath so ~/.local/bin is on PATH. Each tool upgrades and uninstalls independently, and none of them can break another.
If instead the module is imported by a script that runs as root under /usr/bin/python3 — a monitoring hook, a backup job — the honest answer is the distro package, because apt will then track its security updates too:
apt-cache policy python3-requests
sudo apt install python3-requestsThe override, and what it actually risks
pip's escape hatch is --break-system-packages (or PIP_BREAK_SYSTEM_PACKAGES=1 in the environment). The name is deliberate: the packaging specification asks installers to make the flag "carry some connotation that its use is risky". In a throwaway container built from ubuntu:24.04, where the image is the thing you discard, it is a defensible shortcut. On a long-lived host it means a future pip install --upgrade can replace a module unattended-upgrade depends on, and you find out at the next patch window. The Homebrew docs give the same warning for macOS, including pip install --upgrade pip against the base interpreter.
A real case: a cron report script after moving to Ubuntu 24.04
A nightly report ran for years on 22.04 from pip3 install --user requests openpyxl. Rebuilt on 24.04, the first pip3 install --user was refused with the text above, and cat /usr/lib/python3.12/EXTERNALLY-MANAGED confirmed the marker. The fix was a venv next to the script and a crontab that calls it by path:
sudo python3 -m venv /opt/report/.venv
sudo /opt/report/.venv/bin/pip install -r /opt/report/requirements.txt# /etc/cron.d/report
15 2 * * * report /opt/report/.venv/bin/python /opt/report/run.pyThe script ran unchanged, and the venv survives the next Python security update because apt never touches /opt.
Confirm the interpreter, then keep it that way
Most follow-up trouble is installing into one interpreter and running another. Ask Python directly:
.venv/bin/python -c 'import sys, requests; print(sys.prefix != sys.base_prefix, requests.__version__)'
.venv/bin/pip --versionTrue 2.34.2
pip 24.0 from /home/you/app/.venv/lib/python3.12/site-packages/pip (python 3.12)True means a virtual environment; a pip path under .venv means installs go where you run from. To keep it that way, give every project a requirements.txt and a venv, and point services at .venv/bin/python by absolute path instead of relying on activation. Leave the marker file alone — apt restores it anyway. In Dockerfiles, prefer the official python:3.12 images — I checked python:3.12-slim and its stdlib directory has no marker — or create a venv in the image. Don't upgrade the system pip with pip.
How this differs from EACCES and ModuleNotFoundError
PermissionError: [Errno 13] Permission denied on /usr/lib/python3/dist-packages is a filesystem permission problem, and pip's message says so — the marker error mentions no path. ModuleNotFoundError: No module named 'requests' after a successful install is the opposite failure: the install worked, into a different interpreter than the one running your script, and which python versus the script's shebang resolves it. Next time this error shows up, walk the questions back in order: which interpreter, which kind of package, which of the three supported paths.
Related questions
Can I just delete the EXTERNALLY-MANAGED file?
You can, and pip installs again — until the package that owns the file is updated. On Ubuntu 24.04 it belongs to libpython3.12-stdlib, so the next Python security update puts it back and your fix silently reverts. You also lose the guard the file exists for. A venv or pipx takes a minute and stays fixed.
Why did pip install --user stop working after an OS upgrade?
The marker arrived with the distro release (Debian 12, Ubuntu 23.04 and later) and pip 23.0 was the first release to honour it. PEP 668 blocks --user on purpose: the user site-packages directory sits on the system interpreter's sys.path just like dist-packages does.
Is --break-system-packages acceptable inside a Dockerfile?
In a disposable image built from a distro base, the damage is contained to the image, so it is a defensible shortcut. Cleaner options are the official python images — python:3.12-slim has no marker in its stdlib directory — or creating a venv inside the image.
I'm on macOS with Homebrew and see the same error. Same fix?
Yes. The Homebrew docs say Homebrew marks its current Python as externally managed per PEP 668, recommend a venv for project dependencies and pipx for applications, and tell you not to run pip install --upgrade pip against the base interpreter.
My venv's pip installed the package, but the script still raises ModuleNotFoundError.
The script is running under a different interpreter. Compare the shebang in the script's first line with which python, and call it explicitly as .venv/bin/python script.py; that removes the guesswork about which interpreter is active.
References
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
More in this category
Git: fatal: detected dubious ownership in repository
Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.
Kubernetes: Internal error occurred: failed calling webhook
An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)
The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.
Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang
The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.