Python / Packaging / Developer Tooling
venv,
pip, pipx
pipx: isolated CLI tool installer · uv: the fast newcomer
PEP 405 (2012) · PEP 668 (2023, "externally managed")
Python has a packaging problem — or rather, it has had several, at different points in its history. The virtual environment is the solution the ecosystem settled on: a self-contained directory that gives each project its own Python interpreter, its own packages, and no ability to contaminate anything else. Understanding why it exists requires understanding what it replaced.
A Python installation is a single interpreter with a single set of installed packages living in a single directory. When you install a package with
pip install requestssite-packagesThe canonical failure mode: Project A was built in 2021 against
Django==3.2Django==4.2pip↳ PEP 668: the externally managed environment
Since Python 3.11 and formalised in PEP 668, most Linux distributions mark their system Python as "externally managed." Attempting to
pip installpip installA virtual environment is a directory — nothing more. It contains a copy of (or symlink to) a Python interpreter, a fresh
site-packagespythonpipsite-packagesmyproject/ ├── .venv/ # convention: name it .venv │ ├── bin/ # Scripts/ on Windows │ │ ├── python # symlink → system interpreter │ │ ├── python3 # symlink → same │ │ ├── pip # pip scoped to this env │ │ └── activate # source this to activate │ ├── lib/ │ │ └── python3.12/ │ │ └── site-packages/ # packages land here │ │ ├── requests/ │ │ └── ... │ └── pyvenv.cfg # home = /usr/bin/python3 ├── requirements.txt └── main.py
The
pyvenv.cfghome = /usr/bin/python3include-system-site-packages = falseThe
venv# Create a virtual environment in .venv/ python3 -m venv .venv # Activate it — modifies PATH for this shell session source .venv/bin/activate # Your prompt now shows (.venv) to indicate activation (.venv) $ which python /home/user/myproject/.venv/bin/python # Install packages into the venv (.venv) $ pip install requests flask # Freeze current dependencies to a file (.venv) $ pip freeze > requirements.txt # Deactivate — restores the original shell environment (.venv) $ deactivate # Delete the venv entirely — it's just a directory $ rm -rf .venv
# Create python -m venv .venv # Activate (PowerShell) .venv\Scripts\Activate.ps1 # Activate (cmd.exe) .venv\Scripts\activate.bat # PowerShell execution policy may block scripts — fix with: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser # Install, freeze, deactivate — identical to Linux (.venv) PS> pip install requests (.venv) PS> pip freeze > requirements.txt (.venv) PS> deactivate
↳ Never commit .venv to version control
The
.venv.venv/.gitignorerequirements.txtpip install -r requirements.txt
pipsite-packages# Install a package (into active venv, or globally if none active) pip install requests # Install a specific version pip install "django==4.2.7" # Install with version constraints pip install "flask>=2.0,<3.0" # Install from a requirements file pip install -r requirements.txt # Install in editable mode (for local development) pip install -e . # List installed packages pip list # Show details about a package pip show requests # Uninstall a package pip uninstall requests # Upgrade pip itself pip install --upgrade pip # Install GLOBALLY — bypass venv, use with caution # (blocked on PEP 668 systems without --break-system-packages) pip install --break-system-packages requests # ← usually wrong
The
--break-system-packagespipx↳ pip install globally vs. inside a venv
The same
pip install requests.venv/lib/pythonX.Y/site-packages/pip install
piprequestsflasknumpyblackruffhttpieyoutube-dlA library installed into a project's venv is used by that project's code. A CLI tool, by contrast, should be available globally — you want to type
black .pip install black
pipx# Install pipx itself (bootstrapping — one of the few valid global installs) pip install --user pipx # user-level, not system pipx ensurepath # adds ~/.local/bin to PATH # Or via system package manager (preferred on Linux) apt install pipx # Debian/Ubuntu brew install pipx # macOS # Install a CLI tool — gets its own isolated venv automatically pipx install black pipx install ruff pipx install httpie pipx install poetry # The tool is now available globally black --version black, 24.3.0 (CPython 3.12.2) # Upgrade a specific tool pipx upgrade black # Upgrade all tools at once pipx upgrade-all # List installed tools and their environments pipx list # Uninstall cleanly — removes the entire isolated venv pipx uninstall black # Run a tool once without installing it permanently pipx run cowsay "hello from a temporary venv"
The rule of thumb: use pip inside a venv for libraries your code imports. Use pipx for tools you run from the command line.
The Python packaging ecosystem has historically been fragmented —
pipvirtualenvcondapoetrypdmhatchuvruffpipvenvpyenv# Install uv curl -LsSf https://astral.sh/uv/install.sh | sh # Create a venv (same result as python -m venv, but faster) uv venv .venv # Install packages — resolves and installs in milliseconds uv pip install requests flask # Install from requirements.txt uv pip install -r requirements.txt # Full project workflow (replaces pip + venv + pyenv) uv init myproject uv add requests # adds to pyproject.toml, installs uv run python main.py # runs in the project's managed env # Install and manage Python versions uv python install 3.12 uv python install 3.11
| Task | pip + venv | pipx | uv |
|---|---|---|---|
| Create venv | python -m venv .venv | automatic (per tool) | uv venv .venv |
| Install library | pip install X | not intended for | uv pip install X |
| Install CLI tool | pip install X (messy) | pipx install X ✓ | uv tool install X |
| Global install | pip install (risky) | isolated globally ✓ | uv tool install |
| Speed | baseline | baseline | 10–100× faster |
| Python version mgmt | no (needs pyenv) | no | yes, built-in |
| Lock file | requirements.txt | N/A | uv.lock |
| Stdlib dependency | yes (Python 3.3+) | no (install separately) | no (install separately) |
Python packaging confusion usually stems from one of three misunderstandings: not knowing whether a venv is active when running pip, not knowing which Python a command resolves to, or treating CLI tools the same as libraries. Once those three distinctions are clear, the rest follows.
The simplest defensible workflow for a new project: create a
.venvpython3 -m venv .venvpippip freeze > requirements.txtpipxuv✓ The decision tree
Is it a library your code imports? →
pip installpipx installuvThe virtual environment is not a workaround for a broken packaging system. It is the correct abstraction for the problem — each project gets its own isolated dependency graph, and the system stays clean. The tools that manage it have proliferated because Python is used for everything from embedded scripts to enterprise web services, and different contexts have different needs. The venv itself, however, is always the same: a directory, a symlink, a modified PATH, and the simple guarantee that what you install here stays here.