Managing Dependencies with pip and Lock Files
20 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A project depends on packages, and those packages depend on more. If each machine picks versions on its own, the same code can behave differently. Dependency management keeps the versions under your control.
Direct and indirect dependencies
Transitive dependency is a package you did not ask for, installed because a package you did ask for needs it. Here tzdata arrives because python-dateutil needs it.
You ask pip for one package. That package may need others, and those may need more. pip installs the whole set.
python -m pip install python-dateutilCollecting python-dateutil Downloading python_dateutil-4.0.0-py3-none-any.whl Collecting tzdata Downloading tzdata-2026.6-py3-none-any.whl Installing collected packages: tzdata, python-dateutil Successfully installed python-dateutil-4.0.0 tzdata-2026.6
You asked for python-dateutil. pip also brought tzdata, because python-dateutil needs it.
Version rules
Version specifier is the rule after a package name, such as ==1.2.0 or >=1.2, that says which versions are allowed. pip picks a version that satisfies every rule.
After a package name you can limit the versions pip may choose. Libraries use loose rules. Applications often pin one exact version.
python-dateutil==4.0.0python-dateutil>=2.8python-dateutil>=2.8,<5python-dateutil~=2.8Example 1 · Reading version rules
rules = ["python-dateutil==4.0.0", "requests>=2.30", "rich~=13.7", "flask"]
for rule in rules: if "==" in rule: kind = "pinned" elif any(sign in rule for sign in (">", "<", "~")): kind = "range" else: kind = "anything" print(f"{rule:<24} {kind}")python-dateutil==4.0.0 pinned requests>=2.30 range rich~=13.7 range flask anything
What this code does
- A rule with
==fixes one version. Rules with>,<or~allow a range. - A bare name accepts any version, which is the riskiest choice.
== allows one version. >= allows that version or newer. A comma joins rules, and all must hold. ~=2.8 means 2.8 or newer, but below 3.0.
pip freeze and requirements.txt
requirements.txt is a text file with one package and version rule per line. pip install -r requirements.txt installs every line, so a teammate can rebuild your environment.
pip freeze lists every package in the active environment, with its exact version. Saved to a file, the list lets a teammate rebuild the same environment.
python -m pip freezepython-dateutil==4.0.0 tzdata==2026.6
Example 2 · Parsing a requirements file
text = """requests==2.31.0rich==13.7.1pydantic>=2.0flask"""
pins = {}for line in text.splitlines(): if "==" in line: name, version = line.split("==") pins[name] = version
print(pins){'requests': '2.31.0', 'rich': '13.7.1'}What this code does
- Each line is a package and a rule. Only the
==lines give an exact version. - Lines with ranges or no version are skipped here, which is why a lock file is better: it pins every line.
Example 3 · Rebuild an environment from the file
Runs in: Your terminal
python -m pip install -r requirements.txtCollecting python-dateutil==4.0.0 Downloading python_dateutil-4.0.0-py3-none-any.whl Collecting tzdata==2026.6 Downloading tzdata-2026.6-py3-none-any.whl Installing collected packages: tzdata, python-dateutil Successfully installed python-dateutil-4.0.0 tzdata-2026.6
What this code does
- Run this in a new, empty virtual environment.
- Both packages come at the exact versions in the file, so the result matches the original.
Save it with python -m pip freeze > requirements.txt. Rebuild with python -m pip install -r requirements.txt. Always do this inside a virtual environment, or the list will hold every package on your computer.
Lock files
Lock file is a file that records the exact version of every package, direct and indirect, that worked for your project. Installing from it gives the same environment on every machine.
Ranges say what is allowed. A lock file records the exact version of every package, direct and indirect, that was tested. Write python-dateutil>=2.8 in requirements.in, run pip-compile requirements.in (from pip-tools), and it writes requirements.txt:
## This file is autogenerated by pip-compile with Python 3.13# by the following command:## pip-compile requirements.in#python-dateutil==4.0.0 # via -r requirements.intzdata==2026.6 # via python-dateutilExample 4 · A range, and what a lock fixes
available = [(2, 7), (2, 8), (2, 9), (3, 0), (5, 0)]low, high = (2, 8), (5, 0)
allowed = [v for v in available if low <= v < high]print("allowed:", allowed)print("pip picks:", max(allowed))
available.append((3, 1))print("next month pip picks:", max(v for v in available if low <= v < high))print("lock file still says: (3, 0)")allowed: [(2, 8), (2, 9), (3, 0)] pip picks: (3, 0) next month pip picks: (3, 1) lock file still says: (3, 0)
What this code does
- The rule
>=2.8,<5allows three versions, and pip picks the newest. - When version 3.1 is released, a fresh install changes. A lock file stops that drift.
Every package is pinned, and each # via note says who needs it. uv and Poetry do the same job with their own commands and lock files.
Keeping the environment healthy
Check the environment from time to time. pip check reports packages whose needs are not met. pip list --outdated shows newer versions. Upgrade on purpose, run your tests, then write a new lock.
python -m pip checkpython -m pip list --outdatedNo broken requirements found. Package Version Latest Type ------- ------- ------ ----- pip 25.2 26.3 wheel
Commit the lock file, not the .venv folder. Keep tools such as mypy in a dev group, away from what users need.
Next step
Check your understanding
8 quick questions