Skip to content
System nominalUTC --:--:--Articles 041
MODULE 01 Lesson 01 / 18 35 MIN

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.

PartWhat lives thereWho 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.

Lab 25 minutes

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.