This is a practical, opinionated guide to Python virtual environments on POSIX/macOS/Linux with a few Windows notes. It shows the cleanest workflows that don't bite you later: create/activate, install via pip and requirements.txt, run scripts with reliable logging/printing, deactivate/uninstall/remove, common gotchas, when to upgrade pip/setuptools/wheel, and a Zsh prompt that shows the active venv in red. It also explains what a venv does and doesn't do (spoiler: you can still build Go, run Docker, etc.).
Python versions and packages vary per project. A venv creates an isolated interpreter + site-packages so each project can pin its own dependencies without polluting system or Homebrew installs. On macOS/Homebrew you usually only have python3 globally; once you activate a venv, you get a project-local python.
which -a python3
python3 -V- Use
python3to create venvs outside an env. - After activation, use
pythonandpython -m pipto guarantee pip lines up with the current interpreter.
# in your project root
python3 -m venv .venv
# activate in zsh/bash
source .venv/bin/activate
# verify
python -V
python -c 'import sys; print(sys.executable)'Windows (PowerShell):
py -m venv .venv
.\.venv\Scripts\Activate.ps1Recommendation: keep the venv inside the repo root as
.venv/(hidden, tidy, works well with editors/CI). Add.venv/to.gitignore.
You don't need to upgrade these for every project. Do it only if installs fail or you want to preempt issues. Best options:
-
One-and-done at creation:
python3 -m venv --upgrade-deps .venv
Seeds the venv with up-to-date
pip(and friends). -
Only if needed (on failure):
python -m pip install -r requirements.txt # if errors: python -m pip install -U pip setuptools wheel python -m pip install -r requirements.txt -
Teams/CI: pin via a constraints file:
# constraints.txt pip>=25 setuptools>=70 wheel>=0.43python -m pip install -r requirements.txt -c constraints.txt
Don't list
pipinrequirements.txt. Use--upgrade-depsor a constraints file instead.
python -m pip install requests beautifulsoup4# generate from current env
python -m pip freeze > requirements.txt
# recreate on a fresh machine/CI
python -m pip install -r requirements.txtStricter, reproducible builds with hashes (optional):
python -m pip install --require-hashes -r requirements.txtIf logs appear delayed, mixed, or out of order, use unbuffered I/O and set UTF-8 as needed.
CLI tips:
# live logs to stdout (unbuffered)
python -u your_script.py
# or via env var
PYTHONUNBUFFERED=1 python your_script.py
# ensure UTF-8 I/O (if you see encoding glitches)
PYTHONUTF8=1 python your_script.py
# or
PYTHONIOENCODING=utf-8 python your_script.pyIn-code logging baseline:
import logging, sys
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
stream=sys.stdout,
force=True, # override prior logging.basicConfig calls
)Module entry point pattern:
python -m yourpkg.cli-
Does:
- Prepends
.venv/binto PATH, sopython,pip, and any console scripts installed in the venv are found first. - Sets a few Python-related vars (e.g.,
VIRTUAL_ENV,PYTHONHOMEunset).
- Prepends
-
Does not:
- Sandbox your entire shell. You can still run Go, Node, Docker, Git, etc.
- Change your
GOROOT/GOPATH, Homebrew, or system tools.
So yes: while (venv) is active you can do:
go build ./golang-search # fine
docker compose up # fine
node script.js # fineFrom Python, subprocess.run(...) inherits the active env. If you need a specific binary outside the venv's PATH shims, call it explicitly (e.g., /usr/local/bin/go) or set env= in subprocess.run.
# leave the venv
deactivate
# uninstall a package (inside the venv)
source .venv/bin/activate
python -m pip uninstall <pkg>
# remove ALL packages but keep the venv structure
python -m pip freeze | xargs -r python -m pip uninstall -y
# nuke the venv directory
deactivate 2>/dev/null || true
rm -rf .venv/-
pipvspip3vs the wrong interpreter Inside a venv, always usepython -m pip. It ties pip to the venv interpreter. -
Global vs user site-packages In a venv,
ENABLE_USER_SITEis usually False. That's intentional isolation. Install everything you need inside the venv. -
Multiple Pythons on PATH (Homebrew/pyenv/system) Create the venv with the exact interpreter you intend:
/opt/homebrew/bin/python3.14 -m venv .venv
-
Entry points/scripts not found Console scripts install into
.venv/bin/. After activation they're on PATH. Without activation, call them explicitly:.venv/bin/your-tool
-
Shebang traps Executable scripts with
#!/usr/bin/env pythonrun whateverpythonis first on PATH. Safer: activate the venv and runpython your_script.py. -
Upgrading Python minor version Venvs are tied to the interpreter version. After upgrading (e.g., 3.12 → 3.14), recreate:
rm -rf .venv python3.14 -m venv .venv source .venv/bin/activate python -m pip install -r requirements.txt -
Faster installs Keep
pip,setuptools,wheelup to date (via--upgrade-depsat creation or on demand). -
"Global" CLI tools without polluting projects Use
pipxfor tools you want to install once and run anywhere.
Disable Python's default (venv) prefix and inject a clean red prefix only when VIRTUAL_ENV is set.
Add to ~/.zshrc:
# Disable Python's default prompt injection
export VIRTUAL_ENV_DISABLE_PROMPT=1
autoload -Uz colors vcs_info
colors
setopt PROMPT_SUBST
# Minimal git right prompt (keep if you like)
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' check-for-changes true
zstyle ':vcs_info:git:*' stagedstr '●'
zstyle ':vcs_info:git:*' unstagedstr '✚'
zstyle ':vcs_info:git:*' formats '%F{244}on%f %F{39}%b%f %F{178}%u%c%f'
zstyle ':vcs_info:git:*' actionformats '%F{244}on%f %F{39}%b%f (%F{208}%a%f) %F{178}%u%c%f'
precmd() { vcs_info }
# Helper: print "(envname) " in red if VIRTUAL_ENV is set
venv_ps1() {
if [[ -n "$VIRTUAL_ENV" ]]; then
local envname="${VIRTUAL_ENV##*/}"
print -r -- "%F{160}(${envname})%f "
fi
}
# Left prompt: (venv) + cwd (last 2 segments), then #/% marker
PROMPT='$(venv_ps1)%F{39}%2~%f %# '
# Right prompt: git info; show exit code on failure
RPROMPT='${vcs_info_msg_0_} %(?..%F{160}↩ %?%f)'Reload cleanly:
exec zsh -l(Use %F{196} for brighter red.)
.PHONY: venv install run test clean
venv:
\tpython3 -m venv --upgrade-deps .venv
install: venv
\t. .venv/bin/activate && python -m pip install -r requirements.txt
run:
\t. .venv/bin/activate && python -u your_script.py
test:
\t. .venv/bin/activate && python -m pytest -q
clean:
\trm -rf .venv __pycache__ .pytest_cache *.pyc-
"permission denied: .venv/bin/activate" You tried to execute it. You must source it:
source .venv/bin/activate -
"bad substitution" after editing
~/.zshrcUse thevenv_ps1function above (no brittle${var:t}tricks). -
pythonnot found (outside venv) On macOS/Homebrew, that's expected. Usepython3outside; usepythoninside venvs. -
Logs delayed/out of order Run unbuffered:
python -u ...orPYTHONUNBUFFERED=1. -
Encoding/emoji issues
PYTHONUTF8=1orPYTHONIOENCODING=utf-8.
echo $ZSH_VERSION # confirm you're in zsh
ps -p $$ -o comm= # "zsh"
set -o | grep posix # should be off
which python3 # e.g., /opt/homebrew/bin/python3
source .venv/bin/activate
which python # .../.venv/bin/python
python -m site# create (outside venvs)
python3 -m venv .venv # or: python3 -m venv --upgrade-deps .venv
# activate (zsh/bash)
source .venv/bin/activate # or: . .venv/bin/activate
# install (tie pip to the interpreter)
python -m pip install -r requirements.txt
# run with live logs
python -u your_script.py # or: PYTHONUNBUFFERED=1 python your_script.py
# deactivate
deactivate
# remove venv
rm -rf .venv/Notes/gotchas:
- Outside venv on macOS/Homebrew: use
python3to create the env. Inside an activated venv: usepythonandpython -m pip. - Activators must be sourced, not executed:
source .venv/bin/activate. - If logs seem buffered:
python -u ...orPYTHONUNBUFFERED=1. - If encoding issues:
PYTHONUTF8=1(3.7+) orPYTHONIOENCODING=utf-8. - Recreate venv after upgrading Python minor version (e.g., 3.12 → 3.14).
- A venv doesn't sandbox your shell—you can still build Go, run Docker, etc.