Skip to content

Python pip Error: externally-managed-environment Fix

pip's error: externally-managed-environment blocks system-wide installs on Debian, Ubuntu, and Homebrew Python. Here's what PEP 668 changed and how to fix it correctly.

Python python-errors pip cpython packaging virtualenv
Bharath G
Reading Progress

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, with ERROR: 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's sys.prefix no longer matches sys.base_prefix, and pip's check short-circuits before it ever looks for the marker file.

Version Behaviour Matrix

Pythonpip behaviourNotes
3.10 (security-only, EOL Oct 2026)Fires if the OS install carries the markerThe check lives in pip, not CPython, so it applies retroactively to any interpreter pip is installed against
3.11 (security-only)SameDebian 12's default interpreter; this is the version most people hit the error on
3.12 (security-only)SameUbuntu 24.04 LTS default; reproduced against this version above
3.13 (bugfix)SameNo interpreter-level change; still purely a pip + OS-marker interaction
3.14 (current bugfix release, Oct 2025)SamePEP 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 policyPEP 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:

  1. running_under_virtualenv() — this is the PEP 405 test: sys.prefix != sys.base_prefix. Every venv/virtualenv sets sys.prefix to the environment's own directory while leaving sys.base_prefix pointing 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 primitive sys.prefix/sys.base_prefix that pip debug, sysconfig, and most "am I in a venv?" one-liners rely on.
  2. The marker file itselfsysconfig.get_path("stdlib") resolves to something like /usr/lib/python3.12, the same directory tree that holds the standard library. If EXTERNALLY-MANAGED exists there, ExternallyManagedEnvironment.from_config(marker) parses it as configparser INI, pulls the Error (or locale-specific Error-<lang>) key, and raises. The exception class hardcodes the surrounding scaffolding — the × This environment is externally managed header, the note: about --break-system-packages, the hint: 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.12

Note 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 now

Before/after:

-python3 -m pip install cowsay
+python3 -m venv .venv && source .venv/bin/activate
+python -m pip install cowsay

Fix 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 cowsay

pipx 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 cowsay

This 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 cowsay

uv 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.txt

Or, 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, always

One 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=true in your CI environment (or the base Docker image) so any pip install that 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-packages as the default — python3 -m venv /opt/venv, ENV PATH="/opt/venv/bin:$PATH", then plain pip install works with zero flags and zero marker interaction, because the venv is now the active interpreter.
  • Lockfiles, not loose requirements: uv.lock, poetry.lock, or pip-tools' compiled requirements.txt — paired with uv sync --frozen or pip install --require-hashes — makes "which venv, which versions" a committed artifact instead of something re-derived per machine.
  • pip check in CI: catches a broken or partially-shadowed environment (a stray system-wide package, a mismatched dependency) before it becomes a runtime ImportError three steps removed from this one.
  • Renovate/Dependabot grouped with your Python version bumps, so a requires-python change 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 uv environment" — 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-environment is pip enforcing PEP 668, not a bug in your package or a broken interpreter — it fires whenever an EXTERNALLY-MANAGED marker file sits in sysconfig.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; pipx is correct for standalone CLI tools; --break-system-packages is 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 sync sidestep the whole question by never defaulting to system site-packages in the first place.
Pythonpython-errorspipcpythonpackagingvirtualenv

From aspiring developer to blogger, I test learning platforms, simplify programming syntax, and share resources that work. Helping you code smarter as I grow myself. New or experienced, you're welcome here.

Comments