Skip to content

Instantly share code, notes, and snippets.

@basperheim
Last active December 7, 2025 15:24
Show Gist options
  • Select an option

  • Save basperheim/17e169478aa1be3cacbd9c530afeb155 to your computer and use it in GitHub Desktop.

Select an option

Save basperheim/17e169478aa1be3cacbd9c530afeb155 to your computer and use it in GitHub Desktop.
Ultimate Python Venv Tutorial and Guide

Ultimate Python Venv Tutorial and Guide

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


Why venvs (in one paragraph)

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.


Prereqs & sanity

which -a python3
python3 -V
  • Use python3 to create venvs outside an env.
  • After activation, use python and python -m pip to guarantee pip lines up with the current interpreter.

Create & activate

# 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.ps1

Recommendation: keep the venv inside the repo root as .venv/ (hidden, tidy, works well with editors/CI). Add .venv/ to .gitignore.


Bootstrapping build tools (pip, setuptools, wheel)

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.43
    python -m pip install -r requirements.txt -c constraints.txt

Don't list pip in requirements.txt. Use --upgrade-deps or a constraints file instead.


Install packages (direct and requirements.txt)

Direct installs (inside the activated venv)

python -m pip install requests beautifulsoup4

From requirements.txt

# generate from current env
python -m pip freeze > requirements.txt

# recreate on a fresh machine/CI
python -m pip install -r requirements.txt

Stricter, reproducible builds with hashes (optional):

python -m pip install --require-hashes -r requirements.txt

Running scripts with correct logging/printing

If 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.py

In-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

What venv does and doesn't do

  • Does:

    • Prepends .venv/bin to PATH, so python, pip, and any console scripts installed in the venv are found first.
    • Sets a few Python-related vars (e.g., VIRTUAL_ENV, PYTHONHOME unset).
  • 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             # fine

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


Deactivating, uninstalling, removing

# 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/

Common gotchas (and fixes)

  • pip vs pip3 vs the wrong interpreter Inside a venv, always use python -m pip. It ties pip to the venv interpreter.

  • Global vs user site-packages In a venv, ENABLE_USER_SITE is 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 python run whatever python is first on PATH. Safer: activate the venv and run python 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, wheel up to date (via --upgrade-deps at creation or on demand).

  • "Global" CLI tools without polluting projects Use pipx for tools you want to install once and run anywhere.


Zsh prompt: show active venv in red (robust, no "bad substitution")

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


Recommended Makefile targets (optional)

.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

Troubleshooting quickies

  • "permission denied: .venv/bin/activate" You tried to execute it. You must source it:

    source .venv/bin/activate
  • "bad substitution" after editing ~/.zshrc Use the venv_ps1 function above (no brittle ${var:t} tricks).

  • python not found (outside venv) On macOS/Homebrew, that's expected. Use python3 outside; use python inside venvs.

  • Logs delayed/out of order Run unbuffered: python -u ... or PYTHONUNBUFFERED=1.

  • Encoding/emoji issues PYTHONUTF8=1 or PYTHONIOENCODING=utf-8.


Sanity checks while working

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

TL;DR

# 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 python3 to create the env. Inside an activated venv: use python and python -m pip.
  • Activators must be sourced, not executed: source .venv/bin/activate.
  • If logs seem buffered: python -u ... or PYTHONUNBUFFERED=1.
  • If encoding issues: PYTHONUTF8=1 (3.7+) or PYTHONIOENCODING=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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment