Virtual Environments and pip 🧪
Half a million packages are waiting for you on PyPI. Two commands stop them from turning your computer into a swamp.
The problem, first
Project A needs version 1 of a library. Project B needs version 2. If both install into the same Python, one of them breaks. Multiply by twenty projects and several years and you get a machine where nothing works and nobody knows why. This has a name: dependency hell.
A virtual environment is a private Python for one project: its own folder, its own installed packages, isolated from everything else. Making one is one command, and you should make one for every project, every time, no exceptions.
The three commands
# 1. create it (once per project)
python3 -m venv .venv
# 2. activate it (every time you open a terminal)
source .venv/bin/activate # macOS and Linux
.venv\Scripts\activate # Windows PowerShell
# 3. install things (they land inside .venv, not on your system)
pip install requests
Once activated, your prompt changes to show it:
$ source .venv/bin/activate
(.venv) $ which python
/Users/you/python-school/.venv/bin/python
(.venv) $ pip install requests
Successfully installed requests-2.32.3 ...
(.venv) $ deactivate
$
The name is a convention, and a strong one: editors like VS Code look for .venv and offer to use it automatically, and every .gitignore template already excludes it. The leading dot hides it from ordinary directory listings.
Never commit it
# .gitignore
.venv/
__pycache__/
*.pyc
.env
A virtual environment contains thousands of files, is specific to your operating system, and can be rebuilt from a text file in seconds. Committing it to git is a classic beginner mistake that makes a repository unusably large.
Recording what you need
(.venv) $ pip freeze > requirements.txt
(.venv) $ cat requirements.txt
certifi==2025.7.9
charset-normalizer==3.4.2
idna==3.10
requests==2.32.3
urllib3==2.5.0
Anyone (including you, on another machine, in a year) can then recreate the exact environment:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
This is the moment your code becomes shareable. Before a requirements file, 'it works on my machine' is a true statement and a useless one. After it, someone else can reproduce your machine in thirty seconds.
Reproducibility is not bureaucracy. It is the difference between a script and software.
pip, the essential commands
| Command | Does |
|---|---|
pip install requests | Install the latest version |
pip install requests==2.32.3 | Install exactly that version |
pip install 'requests>=2.30' | Install at least that version |
pip install -r requirements.txt | Install everything listed in a file |
pip list | What is installed here |
pip show requests | Details, including what depends on what |
pip uninstall requests | Remove it |
pip install --upgrade requests | Update it |
python3 -m pip install x guarantees the package lands in the same Python you are running. Bare pip can belong to a different installation, which produces the single most baffling beginner experience there is: pip says it installed successfully, and Python says ModuleNotFoundError.
Using an installed package
# after: pip install requests
import requests
response = requests.get("https://api.github.com/repos/python/cpython")
data = response.json()
print(data["name"], "has", data["stargazers_count"], "stars")
That block has no run button, because the school's in-browser Python has no network
access and no third-party packages. This is exactly the point at which doing
the ten-minute lab setup starts to pay off. Lesson 42 covers
requests properly.
The modern alternative: uv
uv is a drop-in replacement for pip and venv that is typically 10 to 100 times faster, because it is written in Rust. It is increasingly the default choice in new projects.
# the same three ideas, one tool
uv venv # create .venv
uv pip install requests # install into it
uv run script.py # run with the right environment, no activation needed
# or let it manage the whole project
uv init my-project
uv add requests
Yes, and it is one of the best arguments for learning both languages. uv, ruff (the linter) and Polars (dataframes) are all Rust programs that made the Python ecosystem dramatically faster. The pattern is: write the workflow in Python, write the hot inner loop in Rust. Lesson 51 shows you how to do it yourself, and the Rusty School is next door when you are ready.
Judging a package before you install it
Anyone can publish to PyPI. Packages have been published with names one typo away from popular ones, containing malware. Before installing something you have not heard of:
- Check the name character by character.
requestsis real.request,requstsandpython-requestsare the kind of thing attackers register. This is called typosquatting. - Look at the PyPI page. When was it last released? Does it link to a real source repository?
- Look at the repository. Stars, recent commits, open issues being answered.
- Prefer the standard library when it will do. Zero dependencies is zero supply chain risk.
- Pin your versions in
requirements.txtso an update cannot change under you without you noticing.
Common problems
| Symptom | Cause | Fix |
|---|---|---|
ModuleNotFoundError right after installing | Installed into a different Python | Activate the venv, then python3 -m pip install x |
externally-managed-environment | You are installing into the system Python on Linux or Homebrew | Make and activate a venv. The error is protecting you |
command not found: pip | Not activated, or pip not installed | python3 -m ensurepip, then activate |
| Works in the terminal, not in VS Code | The editor is using a different interpreter | Ctrl+Shift+P, 'Python: Select Interpreter', pick the .venv one |
Permission denied while installing | Trying to write to a system folder | Never use sudo pip. Use a venv |
Set up a real project
On your own machine, do the whole loop: folder, venv, activate, install requests, freeze, and confirm which Python is in charge.
Reveal solution
mkdir weather-tool && cd weather-tool
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requests
python -c "import requests; print(requests.__version__)"
pip freeze > requirements.txt
printf '.venv/\n__pycache__/\n' > .gitignore
which python
deactivateIf which python printed a path ending in weather-tool/.venv/bin/python, everything is correct. That sequence is the opening move of every Python project you will ever start.
Read a requirements file
What does each line mean, and which one would you object to in a code review?
requests==2.32.3
rich>=13.0
pandas
numpy~=1.26.0Reveal solution
requests==2.32.3: exactly this version. Fully reproducible.rich>=13.0: this or newer. A future version 14 could break you.pandas: any version at all. This is the one to object to. Today it installs 2.x; next year it installs 3.x and your program changes behaviour with no change to your code.numpy~=1.26.0: compatible release, so 1.26.x but not 1.27. A sensible middle ground.
Rule of thumb: pin exactly for applications you deploy, use ranges for libraries other people will install alongside things you cannot predict.
Explain it to a colleague
A teammate says: 'Virtual environments are pointless, I just install everything globally and it works fine.' Write the three-sentence reply.
Reveal solution
Something like: It works fine until two projects need different versions of the same library, and then one of them breaks in a way that is genuinely hard to diagnose. It also means nobody else can reproduce your setup, so 'works on my machine' becomes the whole support process. A venv costs one command per project and removes both problems permanently.
The persuasive detail is the second sentence. Most people accept isolation as theory and adopt it for real the first time a colleague cannot run their code.