On This Page
The Error
$ python3 -m pip install cowsay
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.That is pip's own message — no traceback, no Python frames, because pip raises a DiagnosticPipError (ExternallyManagedEnvironment, defined in pip._internal.exceptions) and prints a formatted diagnostic instead of unwinding a stack. The body text between × and the note:/hint: lines is not hardcoded in pip; it's read verbatim from a config file your OS package manager installed, which is why the wording differs by distro. Homebrew's Python on macOS prints the same shape with brew install instead of apt install; Fedora's dnf build says the same thing with dnf.
This is not a bug in your code, your package, or pip. It's Debian's (and Ubuntu's, and Homebrew's) implementation of PEP 668, and it fires on every pip install that isn't running inside a virtual environment, whether or not the package you're installing would actually conflict with anything the OS ships.
Applies to: any pip built against pip ≥ 23.0.1 (released February 2023) running on a Python install that carries an EXTERNALLY-MANAGED marker file — Debian 12 "Bookworm" and later, Ubuntu 23.04 and later (including 24.04 LTS), Fedora 38+, and Homebrew's Python formula from late 2023 onward. Older pip on the same OS, or the same pip against a python.org installer or a pyenv-built interpreter, never shows it, because the check is pip-side but the trigger is the marker file, and neither macOS's own /usr/bin/python3 nor Windows installs ship one.
How to Reproduce It
Minimum repro, no project files needed — this is an environment problem, not a code problem. On Debian 12+, Ubuntu 23.04+ (this example uses 24.04 LTS), or any Docker image based on them:
bash
$ python3 --version
Python 3.12.3
$ python3 -m pip install cowsay
error: externally-managed-environment
...To see it without owning such a machine, reproduce it in a container:
bash
docker run --rm -it ubuntu:24.04 bash -c "
apt-get update -qq && apt-get install -y -qq python3-pip >/dev/null &&
python3 -m pip install cowsay
"You'll get the exact block above. The marker pip is reacting to is a plain text file, not a Python object — you can read it yourself:
bash
$ find / -name EXTERNALLY-MANAGED 2>/dev/null
/usr/lib/python3.12/EXTERNALLY-MANAGED
$ cat /usr/lib/python3.12/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.
...That's an INI file with one section, [externally-managed], and one required key, Error (PEP 668 also allows Error-<lang_code> for localized variants, e.g. Error-de_DE). Delete that one file — don't, but hypothetically — and the check disappears entirely, because pip's only test is "does this file exist."
Environment variables that change what you see:
PIP_REQUIRE_VIRTUALENV=true python3 -m pip install cowsay— fails before pip even checks for the marker, withERROR: Could not find an activated virtualenv (required).A stricter, earlier gate than PEP 668's own.PIP_BREAK_SYSTEM_PACKAGES=1 python3 -m pip install cowsay— same effect as--break-system-packages, set once in CI instead of typed on every invocation.- Running the exact same command inside a
python3 -m venv .venv && .venv/bin/pip install cowsay— no error at all, because the venv'ssys.prefixno longer matchessys.base_prefix, and pip's check short-circuits before it ever looks for the marker file.
Version Behaviour Matrix
| Python | pip behaviour | Notes |
|---|---|---|
| 3.10 (security-only, EOL Oct 2026) | Fires if the OS install carries the marker | The check lives in pip, not CPython, so it applies retroactively to any interpreter pip is installed against |
| 3.11 (security-only) | Same | Debian 12's default interpreter; this is the version most people hit the error on |
| 3.12 (security-only) | Same | Ubuntu 24.04 LTS default; reproduced against this version above |
| 3.13 (bugfix) | Same | No interpreter-level change; still purely a pip + OS-marker interaction |
| 3.14 (current bugfix release, Oct 2025) | Same | PEP 810 (lazy imports) and the tail-call interpreter don't touch packaging behaviour |
| 3.15 (rc phase, final due 1 Oct 2026) | Same, unless your distro changes policy | PEP 751's pylock.toml (lockfile standard) is orthogonal — it changes what you install, not whether the marker check runs |
This one is version-neutral on the CPython side: PEP 668 is a packaging PEP, implemented by pip and enforced by whichever marker file your OS or package manager drops into sysconfig.get_path("stdlib"). Nothing about it changed across 3.10–3.15, and nothing scheduled for 3.15 changes it either. The only axis that matters is pip version (≥ 23.0.1 implements the check at all) and whether your Python install has the marker file — that's a distro packaging decision, not a language version.
Why It Happens — Surface Level
Your system's Python isn't just "a Python interpreter" on these distros — it's a dependency the OS itself relies on. Debian's apt, Ubuntu's unattended-upgrades, and dozens of system tools import from /usr/lib/python3/dist-packages. Before PEP 668, running sudo pip3 install <anything> installed straight into that same directory, silently shadowing or upgrading a package the OS package manager thought it owned. The next apt upgrade could then overwrite your pip-installed version, or worse, pip could remove a package apt still depended on. PEP 668 gives distros a standard way to say "don't do that here" — and pip enforces it unconditionally, for every install, regardless of whether the specific package you're installing would actually collide with anything.
Why It Happens — Under the Hood
The whole mechanism is about fifteen lines of pip source. pip._internal.commands.install.InstallCommand.run() calls check_externally_managed() near the top of the install path, before dependency resolution starts:
python
# pip/_internal/utils/misc.py
def check_externally_managed() -> None:
"""Check whether the current environment is externally managed."""
if running_under_virtualenv():
return
marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
if not os.path.isfile(marker):
return
raise ExternallyManagedEnvironment.from_config(marker)Two checks, in order:
running_under_virtualenv()— this is the PEP 405 test:sys.prefix != sys.base_prefix. Everyvenv/virtualenvsetssys.prefixto the environment's own directory while leavingsys.base_prefixpointing at the system interpreter that created it. If they differ, pip assumes you deliberately isolated yourself and returns immediately — no marker check at all. This is the same primitivesys.prefix/sys.base_prefixthatpip debug,sysconfig, and most "am I in a venv?" one-liners rely on.- The marker file itself —
sysconfig.get_path("stdlib")resolves to something like/usr/lib/python3.12, the same directory tree that holds the standard library. IfEXTERNALLY-MANAGEDexists there,ExternallyManagedEnvironment.from_config(marker)parses it asconfigparserINI, pulls theError(or locale-specificError-<lang>) key, and raises. The exception class hardcodes the surrounding scaffolding — the× This environment is externally managedheader, thenote:about--break-system-packages, thehint:pointing at the PEP — and only the middle block comes from the distro's file.
You can watch both checks directly:
bash
$ python3 -c "import sys; print(sys.prefix, sys.base_prefix)"
/usr /usr # equal -> not in a venv
$ python3 -m venv /tmp/v && /tmp/v/bin/python -c "import sys; print(sys.prefix, sys.base_prefix)"
/tmp/v /usr # different -> venv detected, check skipped entirely
$ python3 -c "import sysconfig; print(sysconfig.get_path('stdlib'))"
/usr/lib/python3.12Note what this means in practice: the check is entirely local to this one pip invocation. It doesn't consult a database of "packages the OS owns" or diff against dpkg's package list — it's a single boolean gate (marker file present, and not in a venv) applied uniformly to pip install foo whether foo is numpy (which Debian also ships as python3-numpy) or some niche package Debian has never heard of. That bluntness is deliberate — PEP 668's authors judged a uniform, occasionally-annoying gate cheaper than pip trying to reason about overlap on every call — but it's also why the error feels disproportionate for a package with zero relationship to anything apt manages.
--break-system-packages doesn't remove the marker or change the detection logic; it's a separate CLI flag (pip/_internal/cli/cmdoptions.py) that the install command checks before calling check_externally_managed(), skipping the call outright. PIP_BREAK_SYSTEM_PACKAGES=1 is the same flag read from the environment instead of argv — useful in a Dockerfile RUN line where you can't (and shouldn't need to) edit every pip install invocation.
The Fix
Quick fix — a virtual environment (recommended for anything but a throwaway container):
bash
python3 -m venv .venv
source .venv/bin/activate # .venv\Scripts\activate on Windows
python -m pip install cowsay # no marker check: sys.prefix != sys.base_prefix nowBefore/after:
-python3 -m pip install cowsay
+python3 -m venv .venv && source .venv/bin/activate
+python -m pip install cowsayFix for a standalone CLI tool you want on your PATH, not imported by other code — pipx:
bash
sudo apt install pipx # or: python3 -m pip install --user pipx (itself unaffected, it's not "install a library")
pipx install cowsaypipx builds one venv per tool automatically, so the tool's dependencies never touch system site-packages, and you still get a cowsay command on your PATH.
Escape hatch — --break-system-packages (use only when you understand the risk):
bash
python3 -m pip install --break-system-packages cowsayThis does exactly what the marker file's author warned you about: it installs straight into the directory apt/dnf also writes to. Reasonable uses are narrow — a throwaway container that's destroyed after the build, or a one-off diagnostic on a box you're about to reimage anyway. Never use it as the default fix on a long-lived machine or shared CI runner; use it project-wide via PIP_BREAK_SYSTEM_PACKAGES=1 only inside a Dockerfile stage that has no other Python-consuming OS packages installed.
Modern alternative — uv, which defaults to venvs and sidesteps the question entirely:
bash
uv venv .venv && uv pip install --python .venv cowsay
# or, for a full project:
uv init myproject && cd myproject && uv add cowsayuv never installs into system site-packages by default, so PEP 668 compliance is a non-issue rather than something you opt into per command.
Best Practices & The Better Design
The fix that actually holds up is: never run pip install against a system interpreter, full stop — with or without the marker file present, on any OS. Debian and Ubuntu are just the distros that now enforce this for you; the underlying risk (a shared, OS-owned Python whose site-packages other software depends on) exists on every platform where you might be tempted to sudo pip install.
bash
# The right way, every time, on any OS:
cd myproject
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txtOr, with a lockfile-based tool instead of a bare requirements.txt:
toml
# pyproject.toml
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["cowsay>=6.1"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"bash
uv sync # creates .venv, resolves, writes uv.lock — one venv per project, alwaysOne virtual environment per project, committed lockfile (uv.lock, poetry.lock, or requirements.txt generated by pip-compile), and CLI tools installed via pipx rather than a bare pip install --user, closes off the whole class of problem this error is warning you about — you're simply never in a position where pip and apt are fighting over the same directory.
Prevent It in the Long-Term
- CI guard: set
PIP_REQUIRE_VIRTUALENV=truein your CI environment (or the base Docker image) so anypip installthat accidentally runs outside an activated venv fails loudly and immediately, with a clearer message than the PEP 668 one, before it ever reaches the marker check. - Dockerfile convention: build a venv explicitly rather than reaching for
--break-system-packagesas the default —python3 -m venv /opt/venv,ENV PATH="/opt/venv/bin:$PATH", then plainpip installworks with zero flags and zero marker interaction, because the venv is now the active interpreter. - Lockfiles, not loose requirements:
uv.lock,poetry.lock, orpip-tools' compiledrequirements.txt— paired withuv sync --frozenorpip install --require-hashes— makes "which venv, which versions" a committed artifact instead of something re-derived per machine. pip checkin CI: catches a broken or partially-shadowed environment (a stray system-wide package, a mismatched dependency) before it becomes a runtimeImportErrorthree steps removed from this one.- Renovate/Dependabot grouped with your Python version bumps, so a
requires-pythonchange and a dependency bump land together and get tested together, rather than a system Python upgrade silently changing whether the marker file exists on your CI image. - Team convention, written down: "we never install into system Python; every project gets its own venv or
uvenvironment" — put it in the README next to the clone instructions, since this error's real fix is a habit, not a flag.
Related: this connects to ModuleNotFoundError and interpreter/venv mismatches (a different pip-vs-Python-path problem, already covered) and to ERROR: Could not build wheels for X (a build-time packaging failure, not an install-policy one) — both belong in the same "which Python am I actually talking to" family this error also comes from.
Important
error: externally-managed-environmentis pip enforcing PEP 668, not a bug in your package or a broken interpreter — it fires whenever anEXTERNALLY-MANAGEDmarker file sits insysconfig.get_path("stdlib")and you're not inside a venv.- The check is exactly two conditions in pip's source:
sys.prefix == sys.base_prefix(not a venv) and the marker file exists — nothing about the specific package you're installing factors in. - A virtual environment is the correct fix in every normal case;
pipxis correct for standalone CLI tools;--break-system-packagesis an escape hatch for disposable containers, not a habit. - The behavior is identical across Python 3.10 through the 3.15 release candidates — it's a pip/packaging-layer decision, not something any CPython version changes.
uv venv/uv syncsidestep the whole question by never defaulting to system site-packages in the first place.
CODELZ Newsletter
Join the newsletter to receive the latest updates in your inbox.