Read this as a study guide instead

pip and virtualenvs

Installing other people's code without breaking your own.

Code with its results

A notebook-style walk through the idea — every output shown is the real result of the code above it.

import: bring a package's names into your program

Import a package and reach its names, pull names with from-import and alias with as, and see each package keep its own namespace.

Real output Every Out block below was produced by running the code above it. You can copy these cells into your own python3 and run them top to bottom to see the same numbers.

A package is code someone else wrote, gathered under a name. import brings its names into your program. Python's standard library ships packages that are always available, so every cell here runs in your own python3 with nothing installed — a pip-installed package imports exactly the same way once it is present.

In [1]
import statistics
readings = [10, 20, 30, 40, 100]
print(statistics.mean(readings))
Out [1]
40

Two ways to trim what you pull in: from statistics import median binds just that one name into your program, and import json as j gives a long package name a short alias you type instead. Both are the same import machinery.

In [2]
from statistics import median
import json as j
print(median(readings))
print(j.dumps({"count": len(readings)}))
Out [2]
30
{"count": 5}

Each package keeps its names in its own namespace, so a name you define and a package's name never collide, and two packages cannot overwrite each other. That per-package separation is the same isolation a virtualenv gives a whole project — one project's packages stay out of another's.

In [3]
import math
mean = 999
print(mean)
print(statistics.mean(readings))
print(math.floor(3.9))
Out [3]
999
40
3

Check your own machine

These commands only read information — they change nothing. Run the block for your OS, then use the table to read your own numbers against the ideas above.

Windows

Which Python, which pip PowerShell
python --version
python -m pip --version
python -m pip list

What you should see First a line like Python 3.12.3. Then a pip line naming the pip version and the site-packages directory it installs into, ending with (python 3.x). Then a two-column table of installed packages under a Package / Version header.

Note If python does nothing or opens the Microsoft Store, Python is not installed on PATH yet; install it from python.org, or use the bundled launcher (py --version). Writing pip as python -m pip guarantees it targets the interpreter you just checked.

The venv tool and where imports resolve PowerShell
python -m venv --help
python -m site

What you should see First the venv module's usage and options for creating a virtual environment (this only prints help; it creates nothing). Then site prints sys.path and the site-packages directories where import finds installed packages.

macOS

Which Python, which pip zsh
python3 --version
python3 -m pip --version
python3 -m pip list

What you should see First a line like Python 3.12.3. Then a pip line naming the pip version and the site-packages directory it installs into, ending with (python 3.x). Then a two-column table of installed packages under a Package / Version header.

Note macOS uses python3 (a bare python may be missing). If pip is absent on the system Python, that is itself the lesson: create a virtual environment and pip comes with it.

The venv tool and where imports resolve zsh
python3 -m venv --help
python3 -m site

What you should see First the venv module's usage and options for creating a virtual environment (this only prints help; it creates nothing). Then site prints sys.path and the site-packages directories where import finds installed packages.

Linux

Which Python, which pip bash
python3 --version
python3 -m pip --version
python3 -m pip list

What you should see First a line like Python 3.12.3. Then a pip line naming the pip version and the site-packages directory it installs into, ending with (python 3.x). Then a two-column table of installed packages under a Package / Version header.

Note On some distributions pip for the system Python is a separate package; if it is missing, creating a virtual environment gives you a working pip inside it.

The venv tool and where imports resolve bash
python3 -m venv --help
python3 -m site

What you should see First the venv module's usage and options for creating a virtual environment (this only prints help; it creates nothing). Then site prints sys.path and the site-packages directories where import finds installed packages.

Where each idea shows up in your output
ConceptWindowsmacOSLinux
Interpreter versionpython --versionpython3 --versionpython3 --version
pip and the folder it installs intopython -m pip --versionpython3 -m pip --versionpython3 -m pip --version
What is installedpython -m pip listpython3 -m pip listpython3 -m pip list
Tool that creates a venvpython -m venv --helppython3 -m venv --helppython3 -m venv --help
Where imports resolve (site-packages on sys.path)python -m sitepython3 -m sitepython3 -m site

The same ideas, as prose

These are the exact fragments the model serves — also available as an ordered study guide.

Other people's code, without the mess

Almost nothing you build in Python is written entirely by you. You reach for code other people already wrote and tested — a library to talk to a web service, to parse a file format, to do the math you would rather not reimplement. That reused code arrives as a package, and pulling one in is the difference between a weekend and a month.

The trouble starts when every project installs its packages into the same shared place. Projects age at different rates: one pins an old version of a library, another needs the newest, and a single shared folder can only hold one version at a time. Install for the second project and you have quietly broken the first. This is the dependency-conflict problem you have met in the abstract, now wearing Python's clothes.

The fix has three moving parts, and the rest of this concept is those three. A virtual environment gives each project its own private package folder instead of the shared one. pip is the installer that fills that folder from the public index of packages. And a requirements.txt file is the written record that lets anyone rebuild the exact same set. Together they let you install freely without any project's dependencies leaking into another's.

A package is code with a name

A package is a bundle of reusable code that lives under a name. Once it is available to your program, you bring its contents in with an import statement, and from then on you reach its functions and values through that name — statistics.mean, json.dumps. The package keeps its own names in its own namespace, so pulling it in never quietly overwrites something you already defined.

Python hands you two very different pools of packages. The first is the standard library — hundreds of packages that ship with Python itself and are always importable, no installation required. json, math, pathlib, random: they are simply there. The second is the enormous ecosystem of third-party packages published to the Python Package Index, or PyPI, the public catalog pip installs from. These do not come with Python; you have to install one before you can import it.

Here is the part worth holding onto: at the import site the two are identical. import requests reads exactly like import json — same statement, same dotted access, same namespace rules. The only difference is that a third-party package had to be installed first, and the standard-library one did not. Everything else in this concept is about making that "installed first" step clean and repeatable.

pip fetches and installs

pip is Python's package installer. Run pip install requests and it goes to PyPI, downloads the requests package along with every other package that requests itself depends on, and places all of them where your Python can import them. One command pulls in a whole tree of code, dependencies and all. pip list prints what is currently installed, and pip uninstall takes one back out.

The subtle part is not the download — it is the destination. pip always installs into one particular Python environment: the one it belongs to. If the pip you run belongs to the system Python, the packages land in the system's shared package folder, which is exactly the shared-folder mess worth avoiding. Writing the command as python -m pip install requests removes the guesswork, because it names the interpreter doing the installing and puts the package wherever that interpreter looks.

So the real question every install raises is "which environment am I pointed at right now?" Get that answer right and pip is a one-line convenience. Get it wrong and you have installed a package into a Python you did not mean to touch. Making that answer obvious and controllable — instead of a thing you hope about — is exactly what a virtual environment is for.

A private environment per project

python pip system site-packages shared, untouched .venv private site-packages no venv active venv active python -m venv .venv makes the private folder; activating sends pip to it
A virtual environment switches where python and pip write: with no venv active they use the shared system site-packages, and with a venv active they use the project-private .venv folder, leaving the system Python untouched.

A virtual environment is a project-private Python. It is an ordinary directory — .venv is the usual name — that holds its own copy of the interpreter and, more importantly, its own package folder. Packages you install while it is in charge go there and nowhere else. The system Python, and every other project, is left exactly as it was like giving each project its own stocked workbench instead of one shared bench where the last person's setup keeps getting in the way.

You create one with python -m venv .venv, which is built into Python and needs nothing installed to work. Creating it only makes the folder; you then activate it to put it in charge. On macOS or Linux that is source .venv/bin/activate; on Windows it is .venv\Scripts\activate. Activation quietly rewires the shell so that python and pip resolve to the copies inside .venv, so from that point on every install lands in the private folder. When you are done, deactivate returns the shell to whatever it was before.

That is the whole trick, and its cheapness is the point. Nothing is registered globally and no system files are touched, so there is nothing to clean up. A virtual environment is just a folder: if one gets into a bad state, you delete it and make a fresh one, and the only thing you have lost is a folder you can rebuild in seconds.

A list that rebuilds the environment

A virtual environment lives on one machine, but the list of what belongs in it should not. A requirements file is that list written down: a plain-text file, conventionally named requirements.txt, with one package per line and usually an exact version pinned to it, like requests==2.31.0. It is not code and it does nothing on its own; it is a record of what a working environment contains like a packing list precise enough that someone else can assemble the identical kit without asking you what went in it.

Two commands move between the environment and the file. pip freeze looks at everything currently installed and prints it in exactly this format, so you capture a known-good setup by saving that output to requirements.txt. Going the other way, pip install -r requirements.txt reads the file and installs every package it names, versions and all. Record with one, reproduce with the other.

Pinning the versions is what turns a rough list into a reliable one. A teammate cloning your project, a server building it fresh, or you returning to it in six months all run the same one-line install and end up with the same packages you did — not merely the same names, but the same versions, which is where "works on my machine" usually goes wrong. The file travels with the project; the installed folder does not have to.

Why the walls are the point

Without a venv: one shared shelf shared site-packages A wants libX 1.0 B wants libX 2.0 collision: one version wins, the other breaks With a venv per project: two shelves A/.venv libX 1.0 B/.venv libX 2.0 each keeps its own version, both projects work
Two projects needing different versions of one package collide in a shared site-packages, where only one version can be installed at a time, but coexist when each project has its own virtual environment.

Two projects on one machine rarely agree on versions. An older project pins a library at the release it was written against; a newer one needs a feature that only exists in the latest. Left in one shared package folder, that disagreement has no good resolution, because a folder can hold only one installed version of a package at a time. Installing the version the new project wants overwrites the version the old project depends on, and the old project breaks the next time it runs — silently, at a distance from the change that caused it.

Give each project its own virtual environment and the disagreement simply stops existing. Each environment has its own package folder holding exactly the versions that project declared, so one project running the old release and another running the new one is no longer a contradiction — they never share the shelf they would fight over. Upgrading a package for one project cannot reach into another, because the reach was the problem and the walls remove it.

This is why the isolation is not a nicety layered on top; it is the whole point of the exercise. The payoff shows up the moment a project has real dependencies. An agent pipeline that pulls in a client for a language-model API, an HTTP library, and a parser is exactly the kind of build whose versions you want pinned in a requirements.txt and sealed inside their own environment — so it keeps running tomorrow, and so anyone can rebuild it from the file and get the pipeline you actually shipped.