A dependency is any library your project needs to run or be developed. Managing dependencies means recording which libraries you need, pinning their versions, installing them reproducibly, and updating them on purpose rather than by accident. Get this right and your project runs on a new machine in one command; get it wrong and “works on my machine” is your most common bug report.
The classic approach is a requirements.txt next to your code. Each line is a package specification: requests==2.31.0 pins exactly, requests>=2.30,<3 allows minor updates, requests is unpinned and mostly a bad idea for code that goes to production. pip install -r requirements.txt installs the whole set.
A more complete story separates direct from transitive dependencies. You wrote code against requests; requests in turn depends on urllib3, certifi and others. Tools like pip-tools, poetry, hatch, and uv let you name only the direct ones and compile a pinned lockfile that includes every transitive one by exact version and hash.
For any project larger than a throwaway script, also distinguish development-only dependencies (linters, test frameworks, type checkers) from runtime ones. Ship users the slim set; developers install the extras with pip install -e ".[dev]" or equivalent. Clear boundaries here pay off the day you have to audit what your users actually receive.
Virtual environments and requirements.txt
The baseline workflow: create a venv, activate it, pip install name what you need, pip freeze > requirements.txt. Commit the file. Everyone else runs pip install -r requirements.txt in their own venv and gets the same set.
pip list --outdated reveals what has new releases; pip install -U name upgrades a specific package. Never blanket-upgrade before a release; changes belong under version control and review.
pyproject.toml and modern tooling
pyproject.toml is the modern configuration file declared by PEP 621. It lists your project name, dependencies, optional groups (for dev, test, docs), and build-system settings. Tools that understand it (pip, build, poetry, hatch, uv) share the same source of truth.
A lockfile (requirements.lock, poetry.lock, uv.lock) freezes every transitive version with a hash. The lockfile is what reproducible installs rely on; the pyproject.toml declares the ranges you accept.
The tools that keep dependencies predictable.
| Tool | Purpose |
|---|---|
pip install -rcommand | Installs every package in requirements.txt. |
pip freezecommand | Outputs pinned versions suitable for requirements.txt. |
pip install -e .command | Installs the current project in editable mode. |
pyproject.tomlfile | Project and dependency configuration. |
pip-toolstool | Compiles requirements.in into a pinned requirements.txt. |
poetrytool | Dependency resolver + build system + publisher. |
uvtool | Fast resolver, installer, and lockfile manager. |
PEP 440spec | Version numbering and range specifiers. |
Managing Project Dependencies code example
The script inspects the active environment and writes a sample requirements.txt in memory so you can see the shape of the file.
# Lesson: Managing Project Dependencies
import io
import sys
from importlib import metadata
from pathlib import Path
from tempfile import TemporaryDirectory
def render_requirements(names: list[str]) -> str:
lines = []
for name in sorted(names):
try:
ver = metadata.version(name)
lines.append(f"{name}=={ver}")
except metadata.PackageNotFoundError:
lines.append(f"# missing: {name}")
return "\n".join(lines) + "\n"
# Pretend these are your direct dependencies
direct = ["pip"]
text = render_requirements(direct)
print("requirements.txt preview:")
print(text)
with TemporaryDirectory() as tmp:
p = Path(tmp) / "requirements.txt"
p.write_text(text, encoding="utf-8")
print("written size:", p.stat().st_size, "bytes")
# Runtime inspection: which Python, which venv?
print("python:", sys.version.split()[0])
print("prefix:", sys.prefix)
print("in venv:", sys.prefix != getattr(sys, "base_prefix", sys.prefix))
What to observe as you run:
1) `metadata.version(name)` is the same data pip sees when it freezes.
2) A requirements.txt is just lines; nothing magical about the format.
3) `sys.prefix` and `sys.base_prefix` differ inside an active venv.
4) A missing package is reported as a comment so the file is still valid.
Draft a two-file split between runtime and dev deps.
# requirements.txt (runtime)
requests>=2.31,<3
pydantic>=2.4
# requirements-dev.txt (development)
-r requirements.txt
pytest>=7
mypy>=1.5
ruff>=0.1
# Install runtime only: pip install -r requirements.txt
# Install dev tools too: pip install -r requirements-dev.txt
A few checks for the format of version specifiers.
from packaging.specifiers import SpecifierSet # ships with pip
assert "2.31.0" in SpecifierSet(">=2.31,<3")
assert "3.0.0" not in SpecifierSet(">=2.31,<3")
from packaging.version import Version
assert Version("1.10") > Version("1.2")
Running prints something like:
requirements.txt preview:
pip==24.0
written size: 10 bytes
python: 3.12.4
prefix: /home/you/.venv
in venv: True