Running Python & installing packages
Run a .py file, isolate dependencies in a virtual environment, and pin them with pip.
The two commands every project starts with
Everything you have written so far ran inside this page. A real project runs somewhere else: a file on your own machine, started from a terminal, leaning on libraries somebody else published. Two commands cover nearly all of it. python3 file.py runs your code, and pip install fetches someone else's. The wrinkle is that pip install has to put the library somewhere, and choosing that somewhere on purpose is the difference between a project that still installs next year and one that breaks the day another project needs a different version.
These commands run in a terminal on your machine, not in the editor on this page. The sandbox here is a Python compiled to WebAssembly: no shell, no pip, no folder to install into. So the graded exercises below check that you can reason about environments in plain Python rather than build one in the browser.
Running a file
Save code in a file whose name ends in .py, then hand that file to the interpreter:
python3 hello.py # macOS and Linux
py hello.py # Windows
Python reads the file top to bottom, exactly as it does here, and exits at the end. Anything you print lands in the terminal. There is no separate compile step to remember and no build output to run instead.
Why one shared install folder goes wrong
pip install requests downloads the library into a directory called site-packages. If every project on your machine shares one site-packages, then every project shares one version of every library, and libraries change. The scraper you wrote last spring pinned to an old API. The dashboard you started yesterday needs the new one. There is exactly one slot, so one of them loses.
What a virtual environment actually is
A virtual environment is a folder. Inside it sit a link to a Python interpreter and, more importantly, its own private site-packages. That is the whole trick: each project gets its own install folder, so project A can hold pandas 1.5 and project B pandas 2.2 while neither knows the other exists.
Create one, then activate it:
python3 -m venv .venv # create the folder .venv in this project
source .venv/bin/activate # macOS and Linux
.venv\Scripts\activate # Windows
Creating is per project and you do it once. Activating is per terminal window: it repoints python and pip in that one shell at the folder, usually adding (.venv) to your prompt so you can see it worked. deactivate puts the shell back. Calling the folder .venv is a convention, not a rule, but it is the one every tool and every teammate expects.
Writing down what you installed
Installing solves today. It does not solve the next person, including you on a new laptop, who needs the same libraries at the same versions. pip freeze prints exactly what is installed, one name==version line per package, and > redirects that into a file you commit:
pip install requests # add a dependency
pip freeze > requirements.txt # write down what is installed right now
pip install -r requirements.txt # rebuild that exact set somewhere else
certifi==2024.7.4
charset-normalizer==3.3.2
requests==2.32.3
Note that requests pulled in two libraries you never asked for. pip freeze lists those too, because reproducing your environment means reproducing all of it.
| Command | What it actually does | How often you run it |
|---|---|---|
| python3 -m venv .venv | Creates a folder with its own interpreter and its own site-packages | Once, when the project is created |
| source .venv/bin/activate | Repoints python and pip in THIS shell at that folder | Every new terminal window |
| pip install requests | Downloads the library into the currently active environment | Whenever you add a dependency |
| pip freeze > requirements.txt | Writes every installed name==version into a file you commit | After you add or upgrade anything |
| pip install -r requirements.txt | Installs exactly those pins into a fresh environment | On a clone, on a new laptop, in CI |
What never gets committed
requirements.txt goes into git. .venv does not. It holds hundreds of megabytes of binaries built for one operating system and one Python version, and pip install -r requirements.txt rebuilds it in seconds, so committing it costs everyone a huge download to receive files that may not even run on their machine. Put a line for it in .gitignore alongside __pycache__/ and *.pyc, the compiled bytecode Python writes next to your source and regenerates whenever it needs to.
The shape of the exercises
Both exercises below work on the text these commands produce, because text is the part you can reason about anywhere. Apply hands you lines that look like pip list output, "requests 2.31.0", and asks for pins. Splitting a line gives you its pieces: "requests 2.31.0".split() returns ["requests", "2.31.0"], so parts[0] is the name and parts[1] is the version. Practice hands you file paths and asks which ones belong in .gitignore.
The tool you will meet at Level 3
uv is a newer, much faster tool that folds venv, pip, and dependency resolution into one binary: uv venv, uv add requests, uv sync. Plenty of new projects start with it. Learn venv plus pip first anyway, because that pair is what you will find in almost every existing repository, Dockerfile, and CI config you inherit. Level 3 picks up uv and pyproject.toml properly.
Interview nuance: "how do you manage dependencies?" is really asking whether you understand reproducibility. The answer that lands is three sentences: one environment per project so versions cannot collide, a committed record of exact versions so any install can be repeated, and never commit the environment itself. Naming the gap in a plain pip freeze file, that it pins whatever happened to be installed rather than what the project actually declares it needs, is what separates a memorized answer from someone who has debugged a broken build.
installed = ["requests 2.31.0", "flask 3.0.0"]
pins = []
for line in installed:
parts = line.split()
pins.append(parts[0] + "==" + parts[1])
print(sorted(pins)) # ['flask==3.0.0', 'requests==2.31.0']Apply
Your turn
The task this lesson builds to.
Implement to_requirements(installed): return the pinned requirement lines for a list of
installed packages.
Each item in installed looks like "requests 2.31.0", a name and a version separated by a
space. Return a list of "name==version" strings, sorted alphabetically.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
A teammate opens a pull request with four thousand changed files: their whole .venv folder
and every __pycache__ directory got committed along with the two lines they meant to change. You
are writing the check that should have caught it.
Implement should_ignore(path): return True when path is generated output that never belongs
in a repository, and False otherwise. A path is ignorable when it starts with ".venv/", when it
contains "__pycache__", or when it ends with ".pyc".
3 hints and 4 automated checks are waiting in the workspace.