Getting a program to run
Installing Python and Virtual Environments
What a Python installation actually contains, which version to choose, why the system copy is off limits, and how to keep projects in their own environments.
The first hour of Python work is usually spent not writing Python. Something in the
tutorial says to run pip install requests, and either the command does not
exist, or it does exist and the next line fails with
ModuleNotFoundError: No module named 'requests'. Both outcomes come from the
same misunderstanding about what a Python installation is.
I have seen this cost an afternoon more than once. A developer installs a package, confirms it in one terminal, opens another, and the import fails. Nothing about the code changed; two different copies of Python were involved, and neither terminal said so.
The fix is not a longer list of commands. It is a mental model: there is no such thing as "Python" on your machine, only particular installations of it, each with its own standard library and its own set of third-party packages. Get that model right once and the setup problems stop repeating.
A Python installation has three parts
The executable you type at the prompt is the smallest part. Around it sits a directory tree that the interpreter consults on every run.
| Part | What lives there | Who changes it |
|---|---|---|
| The interpreter | The python3 binary and the modules written in C that ship with it. |
Nobody, between upgrades. |
| The standard library | os, json, pathlib, sqlite3 and roughly two hundred more modules, installed next to the binary. |
Only by upgrading Python itself. |
site-packages |
Everything you installed: pip puts wheels here. |
You, via pip, ideally inside a virtual environment. |
Two of those three are read-only in practice. That is the fact the rest of this lesson
follows from. When you run pip install, one directory changes: the
site-packages belonging to one interpreter. The other interpreters on the
machine see nothing, and the code you run tomorrow will see nothing either if it happens
to use a different one.
An installation also has a name and a location. The location is called the prefix, and the interpreter will tell you what it thinks it is:
python3 -c "import sys; print(sys.prefix)"
/opt/homebrew/opt/python@3.14/Frameworks/Python.framework/Versions/3.14
Yours will be somewhere else. On macOS, the copy at /usr/bin/python3 is
the one the Command Line Tools install. On Debian and Ubuntu,
/usr/bin/python3 belongs to the distribution's package manager. If you
install Python from Homebrew, from pyenv or from python.org's Windows build, you have
added another installation, with its own site-packages. That is why the next
section matters.
Choose 3.12 or newer, and treat 3.11 as the floor
Version choice is a support question before it is a features question. Reading
sys.version tells you what you have:
python3 --version
Python 3.14.7
Write the course code against Python 3.12 or newer. If a machine already has 3.11, you can work on it; nothing here relies on a feature from later releases. Below 3.11 I would install a newer interpreter rather than adapt, because a Python that no longer receives security fixes is a production risk, not a style preference. Support windows are worth checking before you commit a version to a server, and they are easy to look up.
Version floor
The examples in this course were run on Python 3.14. They use no feature newer than
3.11, so 3.12 and 3.13 behave identically here. If python3 --version
prints something starting with 3.9 or 3.10, install a newer interpreter before
continuing.
What you install is simpler than people expect. You do not install pip
separately, and you do not install venv: both ship with the interpreter. On
some distributions the venv module lives in a separate system package named
python3-venv, which is a packaging choice rather than a separate tool, and
you will see the difference the first time python3 -m venv complains.
The command name is a local decision, not a language rule
There is no Python command called python3. There are files on your
PATH with that name, and whatever is first wins. On macOS and most Linux
systems, python3 exists and python does not, or
python points at a Python 2 build that older distributions still keep around
for their own scripts. On Windows the installer can provide python, and the
launcher py is more reliable because it reads the registry and picks the
newest version you have installed. py -3.12 asks for a specific one.
I do not run bare pip. I put four extra characters in front of it:
python3 -m pip --version
pip 26.2.1 from /opt/homebrew/lib/python3.14/site-packages/pip (python 3.14)
python3 -m pip means "run the pip module inside the
interpreter I just named". A bare pip might belong to a different
installation than the python3 you run on the next line, and then you are
back to the package that cannot be imported. The parent interpreter in the version string
above is the point of the command. Read it. If it does not name the Python you expect,
stop and fix that before installing anything.
On this machine pip is not on the PATH at all, which is
common on macOS with Homebrew:
pip --version
-bash: pip: command not found
The module form works, the bare command does not, and no configuration was wrong. The
pip script was never linked into a directory on the PATH.
The system Python belongs to the operating system
On macOS and on most Linux distributions the Python at /usr/bin/python3
is an operating-system component. Debian and Ubuntu use it for system scripts and
apt-installed tooling. Apple ships it with the Command Line Tools, and parts
of the developer toolchain expect the version that came with them. When you upgrade it,
you change something the OS did not offer to change, and the failures surface later in
software that has nothing to do with your project.
There is no undo for that beyond reinstalling the OS component, and OS updates may
replace the interpreter anyway and leave your packages behind. So when a tutorial tells
you to run sudo pip install, that is not a command to run with more
confidence; it is a sign the tutorial is not written for a machine you care about. The
same advice applies in the other direction: never install project dependencies into an
interpreter that came with the operating system.
A virtual environment is a directory, not a mode
All a virtual environment does is give one project its own
site-packages and its own python command. It is a directory,
usually named .venv, and you can delete it and recreate it in seconds.
cd ~/projects/wordcount
python3 -m venv .venv
source .venv/bin/activate
python -m pip install six
python -m pip freeze > requirements.txt
On Windows in PowerShell the activation line is
.venv\Scripts\Activate.ps1; in cmd.exe it is
.venv\Scripts\activate.bat. Everything else is the same apart from the path
separator.
Once you activate, the shell finds .venv/bin first, so
python means the interpreter inside the environment and
python -m pip installs into
.venv/lib/python3.x/site-packages. Note that this happens because of
PATH ordering, not because Python has an "active environment" setting. There
is no state anywhere else. That is why running .venv/bin/python script.py
works just as well without activating anything, and why a script launched from your editor
with an absolute interpreter path ignores the shell's environment completely. Activation
is a convenience for typing, and that is all it is. Both names work once you are inside,
because venv creates python alongside python3 in
the environment's bin directory.
The one file worth keeping is the freeze output. Virtual environments are disposable
and should never be committed to version control, so requirements.txt is how
the environment survives a machine change. Everything in this course that installs a
dependency does it inside an environment built this way.
Ask the interpreter who it is before you debug anything
Every symptom here reduces to one question: which Python is running? The
interpreter will answer it about itself. Here is a file called
whoami.py:
import sys
import site
print("interpreter:", sys.executable)
print("version: ", sys.version.split()[0])
print("prefix: ", sys.prefix)
print("packages: ", site.getsitepackages())
interpreter: /Users/ana/projects/wordcount/.venv/bin/python
version: 3.14.7
prefix: /Users/ana/projects/wordcount/.venv
packages: ['/Users/ana/projects/wordcount/.venv/lib/python3.14/site-packages']
sys.executable is the path to the binary that is executing the file, which
makes it the only trustworthy answer to the question. sys.prefix says which
installation it belongs to, and if that path starts with your project directory, you are
inside the environment. site.getsitepackages() names the directory that
pip should have written to. If those two disagree with what you installed,
you have found the bug without reading any of the code under test.
Then there is the failure this whole lesson is building toward. You install a library, one terminal imports it, another says the module does not exist. The import error names the interpreter that failed:
Traceback (most recent call last):
File "/Users/ana/projects/wordcount/report.py", line 1, in <module>
import six
ModuleNotFoundError: No module named 'six'
Spend a second on the path in that first frame, which announces the interpreter. If it
is the system one, the install went somewhere else. If it is the environment, then the
install genuinely did not happen, or it happened under a different name. Either way there
is nothing wrong with the code, and no amount of reading report.py will help.
When an import does succeed but the object you expected is missing, the cause is usually a
local file named after the library, and the question to ask is what you actually got
rather than what you asked for:
python -c "import six; print(six.__file__)"
The path it prints should sit under the same prefix that sys.executable
reported. When it does not, the import is not coming from where you think, and every
conclusion you draw afterwards will be wrong.
Build two environments and break the import yourself, on purpose, so that the next time it happens you recognise it in one glance.
Create a directory, make two virtual environments inside it called
.venv-a and .venv-b, and install six into the
first one only. Do not install it into the second.
Then run the same one-liner with each interpreter and record what happens:
.venv-a/bin/python -c "import six; print(six.__version__)"
.venv-b/bin/python -c "import six; print(six.__version__)"
Finally, activate the second environment, run the import through the
python that activation puts on your path, then deactivate and run the same
import again with the python3 you normally use. Answer three questions:
which interpreter ran the failing import, what did activation change, and what would
you type to install six so that .venv-b/bin/python can see
it?
Show the solution and the reasoning
The environment is two directories and one install:
mkdir ~/projects/envlab
cd ~/projects/envlab
python3 -m venv .venv-a
python3 -m venv .venv-b
.venv-a/bin/python -m pip install six
The first one-liner prints a version such as 1.17.0. The second exits
non-zero with ModuleNotFoundError: No module named 'six', and the path in
that traceback names .venv-b/bin/python. That path is the answer to the
first question, and it is the part of the output people skip.
Activation changed which binary you reached for, not what is installed.
source .venv-b/bin/activate prepends that environment's bin
directory to PATH, so a bare python resolves to
.venv-b/bin/python and the import fails exactly as it did when you named the
path yourself. After deactivate, python3 resolves to the system
or Homebrew interpreter again and fails for the same reason, with a different path in the
traceback. Only the path changes.
For the last question, there are two equivalent answers. The short one is to run
.venv-b/bin/python -m pip install six. The other is to activate
.venv-b and run python -m pip install six. They do the same
thing because both name the interpreter first and let the module form of
pip follow it. What I avoid is a bare pip install six, because
it succeeds while writing into whichever interpreter owns the pip script
on the PATH, and that is how the same afternoon gets spent twice.
One more observation is worth making. six was installed from the
network, but nothing about the experiment depends on which package it is. If you are
offline, any package already in your cache works, and the lesson is identical: the
interpreter that runs the code decides what it can import, and no other interpreter has
a vote.
Progress
Mark this lesson done and it stays marked in this browser.