💻AI Codingbeginner

Two Python Traps on Ubuntu 24.04: PEP 668 and Stdlib Name Collision

The externally-managed-environment error and the import that breaks for no reason, with the clean fixes most beginners miss.

By··7 min read
Ubuntu 24.04 terminal showing the externally-managed-environment pip error and a stdlib name collision traceback

I lost an evening to two Python traps on a fresh Ubuntu 24.04 box. Both hit beginners hard, both look like Python is broken, and both have clean fixes once you know the cause. The first is pip flatly refusing to install anything. The second is an import that throws a traceback pointing at code you never wrote. Neither is a bug in Python. Here is what bit me and how I cleared each one.

Trap 1: error: externally-managed-environment

You install Python, you run the first pip install you've ever typed, and you get a wall of red:

pip 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.

This is not a permissions problem and it is not Python being broken. Ubuntu 24.04 ships a marker file named EXTERNALLY-MANAGED next to the system Python, and that marker tells pip to refuse touching system packages. The rule behind it is PEP 668.

Why does Ubuntu do this? The system Python is owned by apt. Half the OS is written against it. If pip overwrites a system package with a newer or incompatible version, you can break apt itself, and then you are debugging a wedged package manager instead of writing code. The marker is a guardrail, not an obstacle.

The right fix is a virtual environment. A venv is a private Python copy that lives in your project, completely isolated from the system one. pip is happy to install into it because nothing in the OS depends on it.

python3 -m venv .venv
source .venv/bin/activate
pip install requests

After source, your shell prompt picks up a (.venv) prefix, and pip install works with zero complaints. When you are done, type deactivate. Add .venv/ to your .gitignore so the environment never gets committed.

Done. The two paths you will see people reach for instead both have sharp edges:

Approach What it does When to use it
python3 -m venv .venv Private per-project Python, fully isolated Always, for project dependencies
pipx install <tool> Installs a command-line tool in its own isolated env, on your PATH Global CLI tools (black, ruff, httpie)
pip install --break-system-packages Overrides PEP 668 and writes into system Python Almost never

pipx is for installing applications you run from the terminal, not libraries you import in a project. If you want ruff or httpie available everywhere, pipx install ruff is correct. But it does not solve the "I need requests inside my script" problem, which is a venv job.

The --break-system-packages flag is the footgun. It does exactly what it says: it tells pip to ignore the guardrail and write into the system Python anyway. It works, which is the trap. The first few times nothing breaks, so it feels fine. Then one day you upgrade a package apt also manages, a system tool that imports it stops working, and now your OS is the bug. Do not use it on your main system Python. The only place I will type it is inside a throwaway container I am about to delete.

Once you are in the venv habit this trap disappears for good. If you want the same isolation discipline applied to a heavier build, the same per-project thinking shows up in building llama.cpp from source.

Trap 2: the import that breaks for no reason

This one cost me more time because the error points away from the actual cause.

I was building a small bot and created a file in the project root to hold UI selectors. I named it selectors.py. Reasonable name. Then I went to set up the environment:

python3 -m venv .venv

It failed. Not with a clear message about my file, but with this:

AttributeError: module 'selectors' has no attribute 'SelectSelector'

I had not touched anything called SelectSelector. I had not even written code that runs yet. The venv never even got created. So why is venv blowing up on an attribute deep inside a module I never imported?

Because selectors is a Python standard library module, and I had just shadowed it. Here is the chain:

  • python3 -m venv imports subprocess (stdlib)
  • subprocess does import selectors (stdlib)
  • Python looks for selectors and finds my selectors.py first, because the current directory is on sys.path by default
  • subprocess then does _PopenSelector = selectors.SelectSelector, my file has no such thing, and it explodes

Python imports the first match on sys.path, and the directory you are running from sits at the front of that list. So any file you put in your project root that shares a name with a stdlib module silently replaces it, for your code and for every tool you run from that directory.

The fix is to rename the file to something that is not a stdlib name and update its imports:

mv selectors.py ui_selectors.py
# before
from selectors import LOGIN_BUTTON

# after
from ui_selectors import LOGIN_BUTTON

Re-running python3 -m venv .venv worked instantly after the rename. That was the entire fix.

To confirm Python is importing the real stdlib module and not your file, check where it resolved from:

import selectors
print(selectors.__file__)

If that prints a path inside your project, you are still shadowing it. It should print something under the system Python, like /usr/lib/python3.12/selectors.py. The same check works for any module: import it and print __file__.

The landmines are the common stdlib names. Do not name a project-root file any of these:

selectors  types  email  string  io  code  token  queue  array
csv  json  signal  socket  ssl  secrets  enum  dataclasses
pathlib  tempfile  subprocess  threading  logging  re  time
datetime  random  math  os  sys  copy  pickle  glob  shutil

Prefer a specific name every time: app_types.py instead of types.py, mail_helpers.py instead of email.py, string_utils.py instead of string.py, ui_selectors.py instead of selectors.py. The bonus footgun is that even if your own code never imports the stdlib name, a tool you launch from the project directory might, which is exactly how venv died on me without my code running.

How to diagnose either one fast

Both traps have a tell once you have seen them.

For Trap 1, read the literal error text. externally-managed-environment is unambiguous. The moment you see it, stop, make a venv, activate it, and re-run pip. Do not start changing permissions or running pip with sudo, both make it worse.

For Trap 2, when an import or a built-in tool throws an AttributeError or ImportError that names a module you did not write, run this from your project root:

ls *.py

Scan that list against the stdlib names above. If one of your files matches, that is your culprit. Confirm with the print(module.__file__) check, rename the file, update imports, and re-run. The signature of this trap is that the traceback points deep inside the standard library at code you never called, while the real problem is a filename sitting right there in your directory.

My standing setup

After that evening I made one habit non-negotiable: a venv per project, created before I install a single package.

mkdir my-project && cd my-project
python3 -m venv .venv
source .venv/bin/activate
echo ".venv/" >> .gitignore
pip install <whatever-i-need>

That single discipline kills Trap 1 permanently, because I never install against system Python again. And the naming reflex from Trap 2 is now automatic: before I save any file in a project root, I check the name against the stdlib list in my head. types.py becomes app_types.py without thinking about it.

Two evenings of pain, two one-line fixes. The cause in both cases was Python doing exactly what it is designed to do, just not what a beginner expects. Set up the venv first, never name a file after a stdlib module, and neither trap touches you again.

Related