Python pip and Virtual Environments (venv)
Every real Python project depends on third-party packages, and every machine that runs Python ends up hosting many different projects with conflicting version needs. pip is Python’s standard package installer, responsible for downloading and installing packages from the Python Package Index (PyPI). venv is the standard library module that creates lightweight, isolated Python environments so that each project can have its own independent set of installed packages. Together they are the foundation of dependency management in Python, and understanding them well saves you from the single most common source of “it works on my machine” bugs.
Overview / How It Works
pip (“pip installs packages”) is a command-line tool bundled with modern Python installations (3.4+). When you run pip install requests, pip contacts PyPI, resolves the dependency graph for that package (and its dependencies’ dependencies), downloads the appropriate wheel or source distribution for your platform, and unpacks it into a site-packages directory that your Python interpreter searches when you write import requests. By default this site-packages directory is tied to whichever Python interpreter you invoked pip with — which is exactly the problem venv solves.
Without isolation, every package you install goes into one global site-packages directory shared by your entire system. If Project A needs django==3.2 and Project B needs django==5.0, they cannot both be satisfied globally at the same time — installing one overwrites the other. Worse, installing packages into the system Python can silently break OS tools that depend on specific versions of libraries already present.
venv solves this by creating a self-contained directory tree that includes its own site-packages folder, its own python executable (usually a lightweight link or copy of the interpreter that created it), and its own copy of pip. When that environment is activated, your shell’s PATH is temporarily rearranged so that typing python or pip resolves to the versions inside the virtual environment instead of the system-wide ones. Packages installed while the environment is active land in the environment’s private site-packages, completely invisible to any other environment or to the system Python. Deactivating restores your original PATH, and deleting the environment’s folder removes every trace of it — nothing else on your system is touched.
Internally, a venv is just a directory. It contains a pyvenv.cfg file recording which base interpreter created it and its version, a bin/ folder (Scripts/ on Windows) holding the python/pip launchers and the activate script, and a lib/pythonX.Y/site-packages/ folder where your installed packages actually live. Python detects at startup whether it was launched from inside a venv by comparing sys.prefix (the active environment’s root) against sys.base_prefix (the original installation’s root) — if they differ, you are inside a virtual environment.
Syntax
Creating and using a virtual environment follows a consistent pattern across platforms, though the activation command differs.
python -m venv ENV_DIRECTORY_NAME
# Linux / macOS (bash or zsh)
source ENV_DIRECTORY_NAME/bin/activate
# Windows (Command Prompt)
ENV_DIRECTORY_NAME\Scripts\activate.bat
# Windows (PowerShell)
ENV_DIRECTORY_NAME\Scripts\Activate.ps1
# Leaving the environment, on any platform
deactivate
- python -m venv — runs the venv module as a script; always prefer this over calling a separate
venvexecutable, since it guarantees the environment is built from the interpreter you intended. - ENV_DIRECTORY_NAME — any folder name you choose (common conventions:
venv,.venv, orenv); venv creates this directory if it does not exist. - activate — a shell script (not a Python file) that modifies your current shell session’s
PATHand prompt; it must be sourced, not executed, on Unix-like shells so the changes affect your current shell rather than a subshell. - deactivate — a function the activate script defines; it restores your original
PATHand prompt.
Once activated, the everyday pip commands are:
| Command | Purpose |
|---|---|
pip install PACKAGE |
Install the latest compatible version of a package |
pip install PACKAGE==1.2.3 |
Install an exact, pinned version |
pip install -r requirements.txt |
Install every package listed in a requirements file |
pip uninstall PACKAGE |
Remove an installed package |
pip list |
Show installed packages and their versions |
pip freeze > requirements.txt |
Write exact installed versions to a file, for reproducible installs |
pip show PACKAGE |
Display metadata (version, location, dependencies) for one package |
pip install --upgrade PACKAGE |
Upgrade a package to its newest compatible version |
Examples
Example 1: Creating a venv and confirming isolation
The typical workflow starts with creating and activating an environment, then verifying that Python is actually running from inside it.
python -m venv project_env
source project_env/bin/activate
python -m pip install --upgrade pip
python check_env.py
deactivate
Where check_env.py contains ordinary Python code that inspects the interpreter’s own state:
import sys
def in_virtualenv() -> bool:
return sys.prefix != sys.base_prefix
print(f"sys.prefix: {sys.prefix}")
print(f"sys.base_prefix: {sys.base_prefix}")
print(f"Running inside a virtual environment: {in_virtualenv()}")
Output (paths will differ on your machine, but the pattern is the same):
sys.prefix: /home/user/myproject/project_env
sys.base_prefix: /usr
Running inside a virtual environment: True
When the venv is active, sys.prefix points inside project_env while sys.base_prefix still points at the system Python installation that created it. If you ran the exact same script without activating the environment, both values would be identical and in_virtualenv() would return False. This is precisely how tools like pip and IDEs detect whether you’re “inside” an environment.
Example 2: Parsing a requirements.txt file
Once your environment has packages installed, pip freeze > requirements.txt captures exact versions so teammates (or a production server) can recreate the same environment with pip install -r requirements.txt. Here’s what that pinning actually looks like from Python’s side:
from pathlib import Path
requirements_content = """requests==2.31.0
rich>=13.0,<14
python-dotenv==1.0.1
"""
req_file = Path("requirements.txt")
req_file.write_text(requirements_content)
for line in req_file.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "==" in line:
name, version = line.split("==")
print(f"{name} is pinned to exactly {version}")
else:
print(f"{line} uses a version range, not pinned")
req_file.unlink()
Output:
requests is pinned to exactly 2.31.0
rich>=13.0,<14 uses a version range, not pinned
python-dotenv is pinned to exactly 1.0.1
The == syntax pins an exact version, which is what pip freeze produces — ideal for reproducible deployments. Range specifiers like >=13.0,<14 are more common in libraries you publish, where you want to allow compatible updates rather than lock consumers to one exact build.
Example 3: Why isolation matters — simulating a version conflict
This example doesn't call pip at all; instead it models, in plain Python, the exact situation venv exists to prevent: two projects that need different versions of the same dependency.
project_a_requirements = {"requests": "2.31.0", "numpy": "1.26.0"}
project_b_requirements = {"requests": "2.19.0", "pandas": "2.2.0"}
def find_conflicts(reqs1: dict[str, str], reqs2: dict[str, str]) -> list[str]:
conflicts = []
for package, version in reqs1.items():
if package in reqs2 and reqs2[package] != version:
conflicts.append(
f"{package}: project A wants {version}, project B wants {reqs2[package]}"
)
return conflicts
conflicts = find_conflicts(project_a_requirements, project_b_requirements)
for conflict in conflicts:
print(conflict)
if not conflicts:
print("No conflicts found")
Output:
requests: project A wants 2.31.0, project B wants 2.19.0
If both projects installed into the same global site-packages, only one version of requests could exist at a time — installing Project B's dependencies would silently break Project A. Giving each project its own venv means each gets its own private site-packages, so both pinned versions can coexist on the same machine without ever interacting.
Under the Hood
When you run python -m venv myenv, Python performs roughly these steps: it creates the directory structure (bin/, lib/pythonX.Y/site-packages/, include/), writes a pyvenv.cfg file recording the path to the base interpreter and whether system site-packages are visible, and places small launcher scripts (or symlinks, on Unix) for python and pip inside bin/. Crucially, it does not copy the entire standard library — the venv's Python still reads the standard library from the original installation, keeping venvs small (a few megabytes) and fast to create.
Activation is pure shell bookkeeping: the activate script prepends the venv's bin/ directory to your PATH environment variable, saves your old PATH and prompt so deactivate can restore them, and (in most shells) changes your prompt to show the environment's name. No Python process is involved in activation itself — it only changes which python and pip your shell finds first the next time you type those commands.
Once inside an active venv, running pip install works exactly as it would systemwide: it resolves and downloads packages from PyPI, but installs them into the venv's own site-packages because that pip binary was built to target that specific interpreter and prefix.
Common Mistakes
Mistake 1: Installing packages globally instead of in a venv. Running pip install pandas without first creating and activating an environment installs the package system-wide, where it can silently clash with other projects or the operating system's own Python tooling. Always create a project-specific environment first:
python -m venv .venv
source .venv/bin/activate
pip install pandas
Mistake 2: Forgetting the environment isn't activated in a new terminal. Activation only affects the shell session it was run in. Opening a new terminal tab or restarting your editor's integrated terminal resets you back to the system Python, and pip install silently goes to the wrong place again. The fix is simply to re-activate in every new shell session before installing or running anything:
source .venv/bin/activate
python check_env.py # confirm sys.prefix points inside .venv
Mistake 3: Committing the venv folder to version control. A virtual environment contains platform-specific binaries and can be large; committing it bloats your repository and breaks on other operating systems. Instead, add the environment folder to .gitignore and commit only a requirements.txt (or pyproject.toml) so anyone can recreate it with pip install -r requirements.txt.
Best Practices
- Create one virtual environment per project, and name it consistently (
.venvis a widely recognized convention that many editors auto-detect). - Add your environment directory to
.gitignore; never commit it. - Always run
python -m pip install --upgrade pipright after creating a new environment — the bundled pip version can lag behind. - Pin exact versions with
pip freeze > requirements.txtfor applications you deploy, so installs are reproducible. - Use looser range specifiers (e.g.
requests>=2.28,<3) inrequirements.txtorpyproject.tomlfor libraries you publish, to avoid forcing exact versions on downstream users. - Check
sys.prefix != sys.base_prefix(or simply look at your shell prompt) before runningpip installif you're ever unsure whether an environment is active. - Delete and recreate an environment rather than trying to "clean" it — environments are cheap and disposable by design.
- Use
python -m pipinstead of a barepipcommand when you need to be certain which interpreter's pip is running, especially on systems with multiple Python installations.
Practice Exercises
- Create a virtual environment named
.venvin a new folder, activate it, and write a Python script that printsTrueif the environment is active andFalseotherwise, using thesys.prefix/sys.base_prefixcomparison shown above. - Inside your activated environment, install any two packages of your choice, then run
pip freezeand save the result torequirements.txt. Deactivate, delete the environment folder, recreate it, and usepip install -r requirements.txtto restore the exact same packages. - Write a Python function
parse_requirement(line: str) -> tuple[str, str | None]that takes one line from a requirements file (like"django==4.2.1"or"click") and returns a tuple of the package name and its pinned version, orNoneif the line has no==pin. Test it against a few sample lines.
Summary
pipis Python's package installer; it downloads and installs packages (and their dependencies) from PyPI into asite-packagesdirectory.venvcreates an isolated directory with its ownsite-packages,python, andpip, so each project can have independent, non-conflicting dependencies.- Activation temporarily changes your shell's
PATHsopython/pipresolve to the environment's own copies;deactivaterestores the previous state. - Python detects an active venv by comparing
sys.prefixtosys.base_prefix. pip freeze > requirements.txtandpip install -r requirements.txttogether give you reproducible environments across machines and teammates.- Never commit environment folders to version control; commit only the requirements file.
