The venv Trap: Why "I Already Installed It" Doesn't Mean What You Think
Two sessions, same error, both times I was sure I was wrong to be confused:
ModuleNotFoundError: No module named 'psycopg2'
I'd installed psycopg2-binary already. I knew I had. First session, I chalked it up to some fluke and reinstalled it... worked, moved on, didn't think about why. Second time, a few days later, fresh SSH connection into the VPS, same error, same confusion, same reflex to reinstall.
That second time is when it actually registered that something structural was going on, not a fluke.
What's actually happening
A Python virtual environment isn't a setting - it's a shell state. When you run python3 -m venv venv and then source venv/bin/activate, you're not configuring the project, you're modifying the current terminal session: swapping which python3 and pip your shell resolves to. Everything pip installs afterward goes into that venv's isolated package directory, invisible to any Python interpreter running outside it.
The part that got me twice: activation doesn't persist. It's not tied to the project directory, not tied to the machine, not remembered anywhere. It's tied to that one shell session. Close the terminal, reconnect over SSH, open a new tab - you're back to a bare system Python that's never heard of psycopg2, no matter how many times you installed it in a session that no longer exists.
The (venv) prefix in your prompt is the entire tell. No prefix, no access. I just wasn't reading it.
The fix, and the habit
#!/bin/bash
source venv/bin/activate
One line. Run before anything else, every new terminal, every new SSH connection, no exceptions. On this project specifically it's actually two lines, since secrets load the same way... export $(cat .env | xargs) right after activation, or the whole run fails a second, different way with missing credentials instead of a missing module.
Twice was enough to turn "wait, didn't I already... " into a reflex: new session, activate first, then do anything else.
Why I'm not annoyed about this
This is a known Debian/Ubuntu Python quirk, not a Radar-specific bug - worth having a name for if it shows up again on a different machine. But it's also exactly the friction that containerizing this service is going to remove outright. Docker doesn't have a "did you remember to activate" step - the environment is baked into the image, consistent every time it runs, no session-scoped state to forget. That's coming later in this build. For now, the manual habit is the fix, and it's a fine one - just a real one, not a "why do I need this" one anymore.