Virtual Environments, requirements.txt, and Reproducibility
Make your project run the same on any machine
In this lesson
Reproducibility means anyone — including future you — can set up your project and get the same result, instead of it only running on your computer. The tools are a virtual environment, a pinned requirements.txt that lists exact package versions, and a short README. This lesson ties them together.
Explain it like I’m 5
A reproducible project is one someone else can set up without guessing what your computer secretly had installed. You write down exactly which packages and versions it needs.
Why “works on my machine” happens
Two computers rarely have the same packages at the same versions, so code that runs for you can break elsewhere. A virtual environment gives each project its own isolated set of packages, and freezing those packages records exactly what it needs.
python -m venv .venv # create an isolated environment
source .venv/bin/activate # activate it (Windows: .venv\Scripts\activate)
pip install requests==2.31.0 # install a pinned dependency
pip freeze > requirements.txt # record exactly what is installed
requirements.txt pins your dependencies
A requirements.txt lists each package with an exact version. Anyone can recreate your environment with pip (pip install -r requirements.txt) and get the same versions you used — no drift, no surprises.
requests==2.31.0
flask==3.0.0
python-dotenv==1.0.1
requirements = """requests==2.31.0
flask==3.0.0
python-dotenv==1.0.1"""
for line in requirements.splitlines():
package, version = line.split("==")
print(f"{package} is pinned to {version}")
requests is pinned to 2.31.0 flask is pinned to 3.0.0 python-dotenv is pinned to 1.0.1
Build a requirements file from your deps
Sometimes you assemble the list yourself from the packages and versions you chose. Sorting by name keeps the file tidy and easy to diff in Git.
Turn a dict of package: version into requirements.txt lines like flask==3.0.0, one per line, sorted by package name.
deps = {"flask": "3.0.0", "requests": "2.31.0", "click": "8.1.7"}
lines = []
# TODO: build a "name==version" line for each package, sorted by name
print("\n".join(lines))
Loop over sorted(deps) and append f"{name}=={deps[name]}" to lines.
deps = {"flask": "3.0.0", "requests": "2.31.0", "click": "8.1.7"}
lines = []
for name in sorted(deps):
lines.append(f"{name}=={deps[name]}")
print("\n".join(lines))
click==8.1.7
flask==3.0.0
requests==2.31.0
Common mistake: Forgetting to activate the virtual environment
Without the activation step, pip install still runs — but installs the package system-wide instead of into the project.
Activate first (source .venv/bin/activate) and confirm with which python that you’re inside the project’s environment before installing.
Common mistake: Not pinning versions
Writing just flask instead of flask==3.0.0 works today, so the version feels optional.
Pin exact versions with ==. Unpinned dependencies drift over time and eventually a new release breaks your project for no obvious reason.
Common mistake: Assuming everyone has the same Python version
Your code runs on your Python, so it’s easy to forget others may be on an older or newer one.
Note the required Python version in the README alongside the setup steps, so others can match it before installing.
What does pinning a version with == protect against?
An exact pin keeps the version stable, so a future install can’t pull a newer, breaking release.
Which command records installed packages into requirements.txt?
pip freeze prints exact installed versions; redirecting it writes them to the file.
Why does a reproducible project need a README with setup steps?
The README captures the human steps — Python version, venv, install — that aren’t obvious from the code alone.
Mini exercise (medium)
Write parse_requirements(text) that reads requirements.txt text and returns a dict mapping each package to its pinned version. Blank lines should be ignored.
Do it here. Finish the code below and press Run to try your answer right in the browser — no setup needed.
def parse_requirements(text):
deps = {}
for line in text.splitlines():
# TODO: skip blank lines; split "name==version" and store it
pass
return deps
text = "requests==2.31.0\n\nflask==3.0.0\n"
print(parse_requirements(text))
Loop text.splitlines(), strip() each line, continue on empties, then name, version = line.split("==") and store it.
def parse_requirements(text):
deps = {}
for line in text.splitlines():
line = line.strip()
if not line:
continue
name, version = line.split("==")
deps[name] = version
return deps
text = "requests==2.31.0\n\nflask==3.0.0\n"
print(parse_requirements(text))
{'requests': '2.31.0', 'flask': '3.0.0'}
assert parse_requirements("flask==3.0.0") == {"flask": "3.0.0"}, "split each line on == into name and version"
assert parse_requirements("a==1\n\nb==2\n") == {"a": "1", "b": "2"}, "blank lines should be ignored"
print("✓ requirements parsed!")