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.

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 venvimportssubprocess(stdlib)subprocessdoesimport selectors(stdlib)- Python looks for
selectorsand finds myselectors.pyfirst, because the current directory is onsys.pathby default subprocessthen 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
More AI Coding

Building a Custom MCP Server in Python: Claude Reaches My Stack
Claude Code is sharp until it hits the edge of your machine and your private tools. I wrote three small MCP servers in Python to close that gap. Here is the real pattern, the real gotcha that bit me, and what it costs.

Claude Code Subagents in Practice: Fork Flag, Cache Leak, Worktree Trap
Fanning out subagents in Claude Code looks free until you hit the cap or your forks clobber each other's commits. These are the real fixes I learned running fanouts: the fork env flag that shares the parent's cache, the WebFetch cache leak, and the worktree pattern for parallel writers.

I Gave My AI Agents a Memory With SQLite FTS5 (No Vector DB)
Most agent-memory setups reach for Pinecone or pgvector by reflex. I put 2000+ markdown files behind SQLite FTS5 with BM25 ranking, and my agents now answer their own 'who is X' questions in under a second for zero tokens. Here is the schema, the query, and the one place lexical search loses.